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.
Files changed (242) hide show
  1. parsek_cdp/__init__.py +55 -0
  2. parsek_cdp/_logging.py +49 -0
  3. parsek_cdp/cdp/__init__.py +247 -0
  4. parsek_cdp/cdp/accessibility/__init__.py +15 -0
  5. parsek_cdp/cdp/accessibility/events.py +35 -0
  6. parsek_cdp/cdp/accessibility/functions.py +202 -0
  7. parsek_cdp/cdp/accessibility/types.py +235 -0
  8. parsek_cdp/cdp/animation/__init__.py +15 -0
  9. parsek_cdp/cdp/animation/events.py +52 -0
  10. parsek_cdp/cdp/animation/functions.py +140 -0
  11. parsek_cdp/cdp/animation/types.py +113 -0
  12. parsek_cdp/cdp/audits/__init__.py +15 -0
  13. parsek_cdp/cdp/audits/events.py +21 -0
  14. parsek_cdp/cdp/audits/functions.py +95 -0
  15. parsek_cdp/cdp/audits/types.py +1051 -0
  16. parsek_cdp/cdp/autofill/__init__.py +15 -0
  17. parsek_cdp/cdp/autofill/events.py +27 -0
  18. parsek_cdp/cdp/autofill/functions.py +64 -0
  19. parsek_cdp/cdp/autofill/types.py +108 -0
  20. parsek_cdp/cdp/backgroundservice/__init__.py +15 -0
  21. parsek_cdp/cdp/backgroundservice/events.py +40 -0
  22. parsek_cdp/cdp/backgroundservice/functions.py +59 -0
  23. parsek_cdp/cdp/backgroundservice/types.py +63 -0
  24. parsek_cdp/cdp/bluetoothemulation/__init__.py +15 -0
  25. parsek_cdp/cdp/bluetoothemulation/events.py +70 -0
  26. parsek_cdp/cdp/bluetoothemulation/functions.py +247 -0
  27. parsek_cdp/cdp/bluetoothemulation/types.py +120 -0
  28. parsek_cdp/cdp/browser/__init__.py +15 -0
  29. parsek_cdp/cdp/browser/events.py +46 -0
  30. parsek_cdp/cdp/browser/functions.py +350 -0
  31. parsek_cdp/cdp/browser/types.py +158 -0
  32. parsek_cdp/cdp/cachestorage/__init__.py +15 -0
  33. parsek_cdp/cdp/cachestorage/events.py +13 -0
  34. parsek_cdp/cdp/cachestorage/functions.py +126 -0
  35. parsek_cdp/cdp/cachestorage/types.py +89 -0
  36. parsek_cdp/cdp/cast/__init__.py +15 -0
  37. parsek_cdp/cdp/cast/events.py +38 -0
  38. parsek_cdp/cdp/cast/functions.py +77 -0
  39. parsek_cdp/cdp/cast/types.py +23 -0
  40. parsek_cdp/cdp/console/__init__.py +15 -0
  41. parsek_cdp/cdp/console/events.py +22 -0
  42. parsek_cdp/cdp/console/functions.py +36 -0
  43. parsek_cdp/cdp/console/types.py +30 -0
  44. parsek_cdp/cdp/css/__init__.py +15 -0
  45. parsek_cdp/cdp/css/events.py +79 -0
  46. parsek_cdp/cdp/css/functions.py +788 -0
  47. parsek_cdp/cdp/css/types.py +742 -0
  48. parsek_cdp/cdp/debugger/__init__.py +15 -0
  49. parsek_cdp/cdp/debugger/events.py +164 -0
  50. parsek_cdp/cdp/debugger/functions.py +663 -0
  51. parsek_cdp/cdp/debugger/types.py +171 -0
  52. parsek_cdp/cdp/deviceaccess/__init__.py +15 -0
  53. parsek_cdp/cdp/deviceaccess/events.py +30 -0
  54. parsek_cdp/cdp/deviceaccess/functions.py +54 -0
  55. parsek_cdp/cdp/deviceaccess/types.py +28 -0
  56. parsek_cdp/cdp/deviceorientation/__init__.py +15 -0
  57. parsek_cdp/cdp/deviceorientation/events.py +13 -0
  58. parsek_cdp/cdp/deviceorientation/functions.py +35 -0
  59. parsek_cdp/cdp/deviceorientation/types.py +10 -0
  60. parsek_cdp/cdp/dom/__init__.py +15 -0
  61. parsek_cdp/cdp/dom/events.py +228 -0
  62. parsek_cdp/cdp/dom/functions.py +1033 -0
  63. parsek_cdp/cdp/dom/types.py +293 -0
  64. parsek_cdp/cdp/domdebugger/__init__.py +15 -0
  65. parsek_cdp/cdp/domdebugger/events.py +13 -0
  66. parsek_cdp/cdp/domdebugger/functions.py +155 -0
  67. parsek_cdp/cdp/domdebugger/types.py +57 -0
  68. parsek_cdp/cdp/domsnapshot/__init__.py +15 -0
  69. parsek_cdp/cdp/domsnapshot/events.py +13 -0
  70. parsek_cdp/cdp/domsnapshot/functions.py +111 -0
  71. parsek_cdp/cdp/domsnapshot/types.py +312 -0
  72. parsek_cdp/cdp/domstorage/__init__.py +15 -0
  73. parsek_cdp/cdp/domstorage/events.py +60 -0
  74. parsek_cdp/cdp/domstorage/functions.py +79 -0
  75. parsek_cdp/cdp/domstorage/types.py +30 -0
  76. parsek_cdp/cdp/emulation/__init__.py +15 -0
  77. parsek_cdp/cdp/emulation/events.py +17 -0
  78. parsek_cdp/cdp/emulation/functions.py +688 -0
  79. parsek_cdp/cdp/emulation/types.py +305 -0
  80. parsek_cdp/cdp/eventbreakpoints/__init__.py +15 -0
  81. parsek_cdp/cdp/eventbreakpoints/events.py +13 -0
  82. parsek_cdp/cdp/eventbreakpoints/functions.py +41 -0
  83. parsek_cdp/cdp/eventbreakpoints/types.py +10 -0
  84. parsek_cdp/cdp/extensions/__init__.py +15 -0
  85. parsek_cdp/cdp/extensions/events.py +13 -0
  86. parsek_cdp/cdp/extensions/functions.py +118 -0
  87. parsek_cdp/cdp/extensions/types.py +19 -0
  88. parsek_cdp/cdp/fedcm/__init__.py +15 -0
  89. parsek_cdp/cdp/fedcm/events.py +45 -0
  90. parsek_cdp/cdp/fedcm/functions.py +93 -0
  91. parsek_cdp/cdp/fedcm/types.py +72 -0
  92. parsek_cdp/cdp/fetch/__init__.py +15 -0
  93. parsek_cdp/cdp/fetch/events.py +82 -0
  94. parsek_cdp/cdp/fetch/functions.py +215 -0
  95. parsek_cdp/cdp/fetch/types.py +81 -0
  96. parsek_cdp/cdp/filesystem/__init__.py +15 -0
  97. parsek_cdp/cdp/filesystem/events.py +13 -0
  98. parsek_cdp/cdp/filesystem/functions.py +36 -0
  99. parsek_cdp/cdp/filesystem/types.py +54 -0
  100. parsek_cdp/cdp/headlessexperimental/__init__.py +15 -0
  101. parsek_cdp/cdp/headlessexperimental/events.py +13 -0
  102. parsek_cdp/cdp/headlessexperimental/functions.py +77 -0
  103. parsek_cdp/cdp/headlessexperimental/types.py +24 -0
  104. parsek_cdp/cdp/heapprofiler/__init__.py +15 -0
  105. parsek_cdp/cdp/heapprofiler/events.py +65 -0
  106. parsek_cdp/cdp/heapprofiler/functions.py +193 -0
  107. parsek_cdp/cdp/heapprofiler/types.py +57 -0
  108. parsek_cdp/cdp/indexeddb/__init__.py +15 -0
  109. parsek_cdp/cdp/indexeddb/events.py +13 -0
  110. parsek_cdp/cdp/indexeddb/functions.py +232 -0
  111. parsek_cdp/cdp/indexeddb/types.py +120 -0
  112. parsek_cdp/cdp/input/__init__.py +15 -0
  113. parsek_cdp/cdp/input/events.py +25 -0
  114. parsek_cdp/cdp/input/functions.py +361 -0
  115. parsek_cdp/cdp/input/types.py +87 -0
  116. parsek_cdp/cdp/inspector/__init__.py +15 -0
  117. parsek_cdp/cdp/inspector/events.py +41 -0
  118. parsek_cdp/cdp/inspector/functions.py +27 -0
  119. parsek_cdp/cdp/inspector/types.py +10 -0
  120. parsek_cdp/cdp/io/__init__.py +15 -0
  121. parsek_cdp/cdp/io/events.py +13 -0
  122. parsek_cdp/cdp/io/functions.py +77 -0
  123. parsek_cdp/cdp/io/types.py +13 -0
  124. parsek_cdp/cdp/layertree/__init__.py +15 -0
  125. parsek_cdp/cdp/layertree/events.py +36 -0
  126. parsek_cdp/cdp/layertree/functions.py +178 -0
  127. parsek_cdp/cdp/layertree/types.py +104 -0
  128. parsek_cdp/cdp/log/__init__.py +15 -0
  129. parsek_cdp/cdp/log/events.py +22 -0
  130. parsek_cdp/cdp/log/functions.py +54 -0
  131. parsek_cdp/cdp/log/types.py +57 -0
  132. parsek_cdp/cdp/media/__init__.py +15 -0
  133. parsek_cdp/cdp/media/events.py +87 -0
  134. parsek_cdp/cdp/media/functions.py +27 -0
  135. parsek_cdp/cdp/media/types.py +101 -0
  136. parsek_cdp/cdp/memory/__init__.py +15 -0
  137. parsek_cdp/cdp/memory/events.py +13 -0
  138. parsek_cdp/cdp/memory/functions.py +164 -0
  139. parsek_cdp/cdp/memory/types.py +71 -0
  140. parsek_cdp/cdp/mixins/__init__.py +21 -0
  141. parsek_cdp/cdp/mixins/datatype.py +123 -0
  142. parsek_cdp/cdp/mixins/event.py +78 -0
  143. parsek_cdp/cdp/mixins/namespace.py +31 -0
  144. parsek_cdp/cdp/network/__init__.py +15 -0
  145. parsek_cdp/cdp/network/events.py +726 -0
  146. parsek_cdp/cdp/network/functions.py +734 -0
  147. parsek_cdp/cdp/network/types.py +1247 -0
  148. parsek_cdp/cdp/overlay/__init__.py +15 -0
  149. parsek_cdp/cdp/overlay/events.py +54 -0
  150. parsek_cdp/cdp/overlay/functions.py +407 -0
  151. parsek_cdp/cdp/overlay/types.py +328 -0
  152. parsek_cdp/cdp/page/__init__.py +15 -0
  153. parsek_cdp/cdp/page/events.py +416 -0
  154. parsek_cdp/cdp/page/functions.py +1081 -0
  155. parsek_cdp/cdp/page/types.py +1088 -0
  156. parsek_cdp/cdp/performance/__init__.py +15 -0
  157. parsek_cdp/cdp/performance/events.py +24 -0
  158. parsek_cdp/cdp/performance/functions.py +63 -0
  159. parsek_cdp/cdp/performance/types.py +22 -0
  160. parsek_cdp/cdp/performancetimeline/__init__.py +15 -0
  161. parsek_cdp/cdp/performancetimeline/events.py +22 -0
  162. parsek_cdp/cdp/performancetimeline/functions.py +30 -0
  163. parsek_cdp/cdp/performancetimeline/types.py +85 -0
  164. parsek_cdp/cdp/preload/__init__.py +15 -0
  165. parsek_cdp/cdp/preload/events.py +116 -0
  166. parsek_cdp/cdp/preload/functions.py +25 -0
  167. parsek_cdp/cdp/preload/types.py +267 -0
  168. parsek_cdp/cdp/profiler/__init__.py +15 -0
  169. parsek_cdp/cdp/profiler/events.py +64 -0
  170. parsek_cdp/cdp/profiler/functions.py +134 -0
  171. parsek_cdp/cdp/profiler/types.py +105 -0
  172. parsek_cdp/cdp/pwa/__init__.py +15 -0
  173. parsek_cdp/cdp/pwa/events.py +13 -0
  174. parsek_cdp/cdp/pwa/functions.py +196 -0
  175. parsek_cdp/cdp/pwa/types.py +45 -0
  176. parsek_cdp/cdp/runtime/__init__.py +15 -0
  177. parsek_cdp/cdp/runtime/events.py +123 -0
  178. parsek_cdp/cdp/runtime/functions.py +552 -0
  179. parsek_cdp/cdp/runtime/types.py +319 -0
  180. parsek_cdp/cdp/schema/__init__.py +15 -0
  181. parsek_cdp/cdp/schema/events.py +13 -0
  182. parsek_cdp/cdp/schema/functions.py +32 -0
  183. parsek_cdp/cdp/schema/types.py +22 -0
  184. parsek_cdp/cdp/security/__init__.py +15 -0
  185. parsek_cdp/cdp/security/events.py +64 -0
  186. parsek_cdp/cdp/security/functions.py +66 -0
  187. parsek_cdp/cdp/security/types.py +168 -0
  188. parsek_cdp/cdp/serviceworker/__init__.py +15 -0
  189. parsek_cdp/cdp/serviceworker/events.py +43 -0
  190. parsek_cdp/cdp/serviceworker/functions.py +115 -0
  191. parsek_cdp/cdp/serviceworker/types.py +95 -0
  192. parsek_cdp/cdp/storage/__init__.py +15 -0
  193. parsek_cdp/cdp/storage/events.py +288 -0
  194. parsek_cdp/cdp/storage/functions.py +571 -0
  195. parsek_cdp/cdp/storage/types.py +656 -0
  196. parsek_cdp/cdp/systeminfo/__init__.py +15 -0
  197. parsek_cdp/cdp/systeminfo/events.py +13 -0
  198. parsek_cdp/cdp/systeminfo/functions.py +75 -0
  199. parsek_cdp/cdp/systeminfo/types.py +132 -0
  200. parsek_cdp/cdp/target/__init__.py +15 -0
  201. parsek_cdp/cdp/target/events.py +109 -0
  202. parsek_cdp/cdp/target/functions.py +424 -0
  203. parsek_cdp/cdp/target/types.py +83 -0
  204. parsek_cdp/cdp/tethering/__init__.py +15 -0
  205. parsek_cdp/cdp/tethering/events.py +22 -0
  206. parsek_cdp/cdp/tethering/functions.py +35 -0
  207. parsek_cdp/cdp/tethering/types.py +10 -0
  208. parsek_cdp/cdp/tracing/__init__.py +15 -0
  209. parsek_cdp/cdp/tracing/events.py +61 -0
  210. parsek_cdp/cdp/tracing/functions.py +119 -0
  211. parsek_cdp/cdp/tracing/types.py +81 -0
  212. parsek_cdp/cdp/webaudio/__init__.py +15 -0
  213. parsek_cdp/cdp/webaudio/events.py +184 -0
  214. parsek_cdp/cdp/webaudio/functions.py +51 -0
  215. parsek_cdp/cdp/webaudio/types.py +155 -0
  216. parsek_cdp/cdp/webauthn/__init__.py +15 -0
  217. parsek_cdp/cdp/webauthn/events.py +69 -0
  218. parsek_cdp/cdp/webauthn/functions.py +214 -0
  219. parsek_cdp/cdp/webauthn/types.py +96 -0
  220. parsek_cdp/core/__init__.py +18 -0
  221. parsek_cdp/core/browser.py +240 -0
  222. parsek_cdp/core/browser_context.py +58 -0
  223. parsek_cdp/core/element.py +405 -0
  224. parsek_cdp/core/feature.py +376 -0
  225. parsek_cdp/core/frame.py +351 -0
  226. parsek_cdp/core/pagable.py +175 -0
  227. parsek_cdp/core/page.py +163 -0
  228. parsek_cdp/core/target.py +490 -0
  229. parsek_cdp/features/__init__.py +14 -0
  230. parsek_cdp/features/request_listener/__init__.py +256 -0
  231. parsek_cdp/features/request_listener/events.py +48 -0
  232. parsek_cdp/features/request_listener/types.py +235 -0
  233. parsek_cdp/generate.py +642 -0
  234. parsek_cdp/parsek/__init__.py +18 -0
  235. parsek_cdp/parsek/events.py +38 -0
  236. parsek_cdp/parsek/types.py +36 -0
  237. parsek_cdp/py.typed +0 -0
  238. parsek_cdp-0.1.0.dist-info/METADATA +277 -0
  239. parsek_cdp-0.1.0.dist-info/RECORD +242 -0
  240. parsek_cdp-0.1.0.dist-info/WHEEL +5 -0
  241. parsek_cdp-0.1.0.dist-info/licenses/LICENSE +201 -0
  242. 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
+ )