cmdop 0.1.21__py3-none-any.whl → 0.1.23__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.
- cmdop/__init__.py +1 -1
- cmdop/_generated/rpc_messages/browser_pb2.py +135 -85
- cmdop/_generated/rpc_messages/browser_pb2.pyi +270 -2
- cmdop/_generated/rpc_messages_pb2.pyi +25 -0
- cmdop/_generated/service_pb2.py +2 -2
- cmdop/_generated/service_pb2_grpc.py +345 -0
- cmdop/client.py +2 -8
- cmdop/services/browser/__init__.py +44 -31
- cmdop/services/browser/capabilities/__init__.py +17 -0
- cmdop/services/browser/capabilities/_base.py +28 -0
- cmdop/services/browser/capabilities/_helpers.py +16 -0
- cmdop/services/browser/capabilities/dom.py +76 -0
- cmdop/services/browser/capabilities/fetch.py +45 -0
- cmdop/services/browser/capabilities/input.py +49 -0
- cmdop/services/browser/capabilities/network.py +245 -0
- cmdop/services/browser/capabilities/scroll.py +147 -0
- cmdop/services/browser/capabilities/timing.py +66 -0
- cmdop/services/browser/js/__init__.py +6 -4
- cmdop/services/browser/js/interaction.py +34 -0
- cmdop/services/browser/models.py +103 -0
- cmdop/services/browser/service/__init__.py +5 -0
- cmdop/services/browser/service/aio.py +30 -0
- cmdop/services/browser/{sync/service.py → service/sync.py} +206 -6
- cmdop/services/browser/session.py +194 -0
- {cmdop-0.1.21.dist-info → cmdop-0.1.23.dist-info}/METADATA +107 -59
- {cmdop-0.1.21.dist-info → cmdop-0.1.23.dist-info}/RECORD +29 -24
- cmdop/services/browser/aio/__init__.py +0 -6
- cmdop/services/browser/aio/service.py +0 -420
- cmdop/services/browser/aio/session.py +0 -407
- cmdop/services/browser/base/__init__.py +0 -6
- cmdop/services/browser/base/session.py +0 -124
- cmdop/services/browser/sync/__init__.py +0 -6
- cmdop/services/browser/sync/session.py +0 -644
- /cmdop/services/browser/{base/service.py → service/_helpers.py} +0 -0
- {cmdop-0.1.21.dist-info → cmdop-0.1.23.dist-info}/WHEEL +0 -0
- {cmdop-0.1.21.dist-info → cmdop-0.1.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
"""Asynchronous browser service."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
from cmdop.services.base import BaseService
|
|
9
|
-
from cmdop.services.browser.base.service import BaseServiceMixin, cookie_to_pb, pb_to_cookie
|
|
10
|
-
from cmdop.services.browser.models import BrowserCookie, BrowserState, PageInfo, ScrollResult, raise_browser_error
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from cmdop.transport.base import BaseTransport
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class AsyncBrowserService(BaseService, BaseServiceMixin):
|
|
17
|
-
"""
|
|
18
|
-
Asynchronous browser service.
|
|
19
|
-
|
|
20
|
-
Example:
|
|
21
|
-
async with client.browser.create_session() as session:
|
|
22
|
-
await session.navigate("https://google.com")
|
|
23
|
-
await session.type("input[name='q']", "Python")
|
|
24
|
-
results = await session.extract(".result-title")
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, transport: BaseTransport) -> None:
|
|
28
|
-
super().__init__(transport)
|
|
29
|
-
self._stub: Any = None
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def _get_stub(self) -> Any:
|
|
33
|
-
if self._stub is None:
|
|
34
|
-
from cmdop._generated.service_pb2_grpc import TerminalStreamingServiceStub
|
|
35
|
-
self._stub = TerminalStreamingServiceStub(self._async_channel)
|
|
36
|
-
return self._stub
|
|
37
|
-
|
|
38
|
-
# === Session Management ===
|
|
39
|
-
|
|
40
|
-
async def create_session(
|
|
41
|
-
self,
|
|
42
|
-
start_url: str | None = None,
|
|
43
|
-
provider: str = "camoufox",
|
|
44
|
-
profile_id: str | None = None,
|
|
45
|
-
headless: bool = False,
|
|
46
|
-
width: int = 1280,
|
|
47
|
-
height: int = 800,
|
|
48
|
-
) -> "AsyncBrowserSession":
|
|
49
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserCreateSessionRequest
|
|
50
|
-
from cmdop.services.browser.aio.session import AsyncBrowserSession
|
|
51
|
-
|
|
52
|
-
request = BrowserCreateSessionRequest(
|
|
53
|
-
provider=provider,
|
|
54
|
-
profile_id=profile_id or "",
|
|
55
|
-
start_url=start_url or "",
|
|
56
|
-
headless=headless,
|
|
57
|
-
width=width,
|
|
58
|
-
height=height,
|
|
59
|
-
)
|
|
60
|
-
response = await self._call_async(self._get_stub.BrowserCreateSession, request)
|
|
61
|
-
|
|
62
|
-
if not response.success:
|
|
63
|
-
raise_browser_error(response.error, "create_session")
|
|
64
|
-
|
|
65
|
-
return AsyncBrowserSession(self, response.browser_session_id)
|
|
66
|
-
|
|
67
|
-
async def close_session(self, session_id: str) -> None:
|
|
68
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserCloseSessionRequest
|
|
69
|
-
|
|
70
|
-
request = BrowserCloseSessionRequest(browser_session_id=session_id)
|
|
71
|
-
response = await self._call_async(self._get_stub.BrowserCloseSession, request)
|
|
72
|
-
|
|
73
|
-
if not response.success:
|
|
74
|
-
raise RuntimeError(f"Failed to close browser session: {response.error}")
|
|
75
|
-
|
|
76
|
-
# === Navigation & Interaction ===
|
|
77
|
-
|
|
78
|
-
async def navigate(self, session_id: str, url: str, timeout_ms: int = 30000) -> str:
|
|
79
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNavigateRequest
|
|
80
|
-
|
|
81
|
-
request = BrowserNavigateRequest(
|
|
82
|
-
browser_session_id=session_id, url=url, timeout_ms=timeout_ms
|
|
83
|
-
)
|
|
84
|
-
response = await self._call_async(self._get_stub.BrowserNavigate, request)
|
|
85
|
-
|
|
86
|
-
if not response.success:
|
|
87
|
-
raise_browser_error(response.error, "navigate", url=url)
|
|
88
|
-
|
|
89
|
-
return response.final_url
|
|
90
|
-
|
|
91
|
-
async def click(
|
|
92
|
-
self, session_id: str, selector: str, timeout_ms: int = 5000, move_cursor: bool = False
|
|
93
|
-
) -> None:
|
|
94
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserClickRequest
|
|
95
|
-
|
|
96
|
-
request = BrowserClickRequest(
|
|
97
|
-
browser_session_id=session_id,
|
|
98
|
-
selector=selector,
|
|
99
|
-
timeout_ms=timeout_ms,
|
|
100
|
-
move_cursor=move_cursor,
|
|
101
|
-
)
|
|
102
|
-
response = await self._call_async(self._get_stub.BrowserClick, request)
|
|
103
|
-
|
|
104
|
-
if not response.success:
|
|
105
|
-
raise_browser_error(response.error, "click", selector=selector)
|
|
106
|
-
|
|
107
|
-
async def type(
|
|
108
|
-
self,
|
|
109
|
-
session_id: str,
|
|
110
|
-
selector: str,
|
|
111
|
-
text: str,
|
|
112
|
-
human_like: bool = False,
|
|
113
|
-
clear_first: bool = True,
|
|
114
|
-
) -> None:
|
|
115
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserTypeRequest
|
|
116
|
-
|
|
117
|
-
request = BrowserTypeRequest(
|
|
118
|
-
browser_session_id=session_id,
|
|
119
|
-
selector=selector,
|
|
120
|
-
text=text,
|
|
121
|
-
human_like=human_like,
|
|
122
|
-
clear_first=clear_first,
|
|
123
|
-
)
|
|
124
|
-
response = await self._call_async(self._get_stub.BrowserType, request)
|
|
125
|
-
|
|
126
|
-
if not response.success:
|
|
127
|
-
raise_browser_error(response.error, "type", selector=selector)
|
|
128
|
-
|
|
129
|
-
async def wait_for(
|
|
130
|
-
self, session_id: str, selector: str, timeout_ms: int = 30000
|
|
131
|
-
) -> bool:
|
|
132
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserWaitRequest
|
|
133
|
-
|
|
134
|
-
request = BrowserWaitRequest(
|
|
135
|
-
browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
|
|
136
|
-
)
|
|
137
|
-
response = await self._call_async(self._get_stub.BrowserWait, request)
|
|
138
|
-
|
|
139
|
-
if not response.success:
|
|
140
|
-
raise_browser_error(response.error, "wait_for", selector=selector)
|
|
141
|
-
|
|
142
|
-
return response.found
|
|
143
|
-
|
|
144
|
-
# === Extraction ===
|
|
145
|
-
|
|
146
|
-
async def extract(
|
|
147
|
-
self, session_id: str, selector: str, attr: str | None = None, limit: int = 100
|
|
148
|
-
) -> list[str]:
|
|
149
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractRequest
|
|
150
|
-
|
|
151
|
-
request = BrowserExtractRequest(
|
|
152
|
-
browser_session_id=session_id,
|
|
153
|
-
selector=selector,
|
|
154
|
-
attribute=attr or "",
|
|
155
|
-
limit=limit,
|
|
156
|
-
)
|
|
157
|
-
response = await self._call_async(self._get_stub.BrowserExtract, request)
|
|
158
|
-
|
|
159
|
-
if not response.success:
|
|
160
|
-
raise RuntimeError(f"Extract failed: {response.error}")
|
|
161
|
-
|
|
162
|
-
return list(response.values)
|
|
163
|
-
|
|
164
|
-
async def extract_regex(
|
|
165
|
-
self, session_id: str, pattern: str, from_html: bool = False, limit: int = 100
|
|
166
|
-
) -> list[str]:
|
|
167
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractRegexRequest
|
|
168
|
-
|
|
169
|
-
request = BrowserExtractRegexRequest(
|
|
170
|
-
browser_session_id=session_id,
|
|
171
|
-
pattern=pattern,
|
|
172
|
-
from_html=from_html,
|
|
173
|
-
limit=limit,
|
|
174
|
-
)
|
|
175
|
-
response = await self._call_async(self._get_stub.BrowserExtractRegex, request)
|
|
176
|
-
|
|
177
|
-
if not response.success:
|
|
178
|
-
raise RuntimeError(f"ExtractRegex failed: {response.error}")
|
|
179
|
-
|
|
180
|
-
return list(response.matches)
|
|
181
|
-
|
|
182
|
-
async def get_html(self, session_id: str, selector: str | None = None) -> str:
|
|
183
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetHTMLRequest
|
|
184
|
-
|
|
185
|
-
request = BrowserGetHTMLRequest(
|
|
186
|
-
browser_session_id=session_id, selector=selector or ""
|
|
187
|
-
)
|
|
188
|
-
response = await self._call_async(self._get_stub.BrowserGetHTML, request)
|
|
189
|
-
|
|
190
|
-
if not response.success:
|
|
191
|
-
raise RuntimeError(f"GetHTML failed: {response.error}")
|
|
192
|
-
|
|
193
|
-
return response.html
|
|
194
|
-
|
|
195
|
-
async def get_text(self, session_id: str, selector: str | None = None) -> str:
|
|
196
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetTextRequest
|
|
197
|
-
|
|
198
|
-
request = BrowserGetTextRequest(
|
|
199
|
-
browser_session_id=session_id, selector=selector or ""
|
|
200
|
-
)
|
|
201
|
-
response = await self._call_async(self._get_stub.BrowserGetText, request)
|
|
202
|
-
|
|
203
|
-
if not response.success:
|
|
204
|
-
raise RuntimeError(f"GetText failed: {response.error}")
|
|
205
|
-
|
|
206
|
-
return response.text
|
|
207
|
-
|
|
208
|
-
# === JavaScript ===
|
|
209
|
-
|
|
210
|
-
async def execute_script(self, session_id: str, script: str) -> str:
|
|
211
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserExecuteScriptRequest
|
|
212
|
-
|
|
213
|
-
request = BrowserExecuteScriptRequest(
|
|
214
|
-
browser_session_id=session_id, script=script
|
|
215
|
-
)
|
|
216
|
-
response = await self._call_async(
|
|
217
|
-
self._get_stub.BrowserExecuteScript, request
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
if not response.success:
|
|
221
|
-
raise RuntimeError(f"ExecuteScript failed: {response.error}")
|
|
222
|
-
|
|
223
|
-
return response.result
|
|
224
|
-
|
|
225
|
-
# === State & Cookies ===
|
|
226
|
-
|
|
227
|
-
async def screenshot(self, session_id: str, full_page: bool = False) -> bytes:
|
|
228
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserScreenshotRequest
|
|
229
|
-
|
|
230
|
-
request = BrowserScreenshotRequest(
|
|
231
|
-
browser_session_id=session_id, full_page=full_page
|
|
232
|
-
)
|
|
233
|
-
response = await self._call_async(self._get_stub.BrowserScreenshot, request)
|
|
234
|
-
|
|
235
|
-
if not response.success:
|
|
236
|
-
raise RuntimeError(f"Screenshot failed: {response.error}")
|
|
237
|
-
|
|
238
|
-
return response.data
|
|
239
|
-
|
|
240
|
-
async def get_state(self, session_id: str) -> BrowserState:
|
|
241
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetStateRequest
|
|
242
|
-
|
|
243
|
-
request = BrowserGetStateRequest(browser_session_id=session_id)
|
|
244
|
-
response = await self._call_async(self._get_stub.BrowserGetState, request)
|
|
245
|
-
|
|
246
|
-
if not response.success:
|
|
247
|
-
raise RuntimeError(f"GetState failed: {response.error}")
|
|
248
|
-
|
|
249
|
-
return BrowserState(url=response.url, title=response.title)
|
|
250
|
-
|
|
251
|
-
async def set_cookies(
|
|
252
|
-
self, session_id: str, cookies: list[BrowserCookie | dict]
|
|
253
|
-
) -> None:
|
|
254
|
-
from cmdop._generated.rpc_messages.browser_pb2 import (
|
|
255
|
-
BrowserCookie as PbCookie,
|
|
256
|
-
BrowserSetCookiesRequest,
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
pb_cookies = [cookie_to_pb(c, PbCookie) for c in cookies]
|
|
260
|
-
request = BrowserSetCookiesRequest(
|
|
261
|
-
browser_session_id=session_id, cookies=pb_cookies
|
|
262
|
-
)
|
|
263
|
-
response = await self._call_async(self._get_stub.BrowserSetCookies, request)
|
|
264
|
-
|
|
265
|
-
if not response.success:
|
|
266
|
-
raise RuntimeError(f"SetCookies failed: {response.error}")
|
|
267
|
-
|
|
268
|
-
async def get_cookies(
|
|
269
|
-
self, session_id: str, domain: str = ""
|
|
270
|
-
) -> list[BrowserCookie]:
|
|
271
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetCookiesRequest
|
|
272
|
-
|
|
273
|
-
request = BrowserGetCookiesRequest(
|
|
274
|
-
browser_session_id=session_id, domain=domain
|
|
275
|
-
)
|
|
276
|
-
response = await self._call_async(self._get_stub.BrowserGetCookies, request)
|
|
277
|
-
|
|
278
|
-
if not response.success:
|
|
279
|
-
raise RuntimeError(f"GetCookies failed: {response.error}")
|
|
280
|
-
|
|
281
|
-
return [pb_to_cookie(c) for c in response.cookies]
|
|
282
|
-
|
|
283
|
-
# === Parser helpers ===
|
|
284
|
-
|
|
285
|
-
async def validate_selectors(
|
|
286
|
-
self, session_id: str, item: str, fields: dict[str, str]
|
|
287
|
-
) -> dict:
|
|
288
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserValidateSelectorsRequest
|
|
289
|
-
|
|
290
|
-
request = BrowserValidateSelectorsRequest(
|
|
291
|
-
browser_session_id=session_id,
|
|
292
|
-
item=item,
|
|
293
|
-
fields=fields,
|
|
294
|
-
)
|
|
295
|
-
response = await self._call_async(
|
|
296
|
-
self._get_stub.BrowserValidateSelectors, request
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
if not response.success:
|
|
300
|
-
raise RuntimeError(f"ValidateSelectors failed: {response.error}")
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
"valid": response.valid,
|
|
304
|
-
"counts": dict(response.counts),
|
|
305
|
-
"samples": dict(response.samples),
|
|
306
|
-
"errors": list(response.errors),
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async def extract_data(
|
|
310
|
-
self, session_id: str, item: str, fields_json: str, limit: int = 100
|
|
311
|
-
) -> dict:
|
|
312
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserExtractDataRequest
|
|
313
|
-
|
|
314
|
-
request = BrowserExtractDataRequest(
|
|
315
|
-
browser_session_id=session_id,
|
|
316
|
-
item=item,
|
|
317
|
-
fields_json=fields_json,
|
|
318
|
-
limit=limit,
|
|
319
|
-
)
|
|
320
|
-
response = await self._call_async(self._get_stub.BrowserExtractData, request)
|
|
321
|
-
|
|
322
|
-
if not response.success:
|
|
323
|
-
raise RuntimeError(f"ExtractData failed: {response.error}")
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
"items": json.loads(response.items_json) if response.items_json else [],
|
|
327
|
-
"count": response.count,
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# === Mouse & Scroll (v2.18.0) ===
|
|
331
|
-
|
|
332
|
-
async def mouse_move(
|
|
333
|
-
self, session_id: str, x: int, y: int, steps: int = 10
|
|
334
|
-
) -> None:
|
|
335
|
-
"""Move cursor to coordinates with smooth movement."""
|
|
336
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserMouseMoveRequest
|
|
337
|
-
|
|
338
|
-
request = BrowserMouseMoveRequest(
|
|
339
|
-
browser_session_id=session_id, x=x, y=y, steps=steps
|
|
340
|
-
)
|
|
341
|
-
response = await self._call_async(self._get_stub.BrowserMouseMove, request)
|
|
342
|
-
|
|
343
|
-
if not response.success:
|
|
344
|
-
raise RuntimeError(f"MouseMove failed: {response.error}")
|
|
345
|
-
|
|
346
|
-
async def hover(self, session_id: str, selector: str, timeout_ms: int = 5000) -> None:
|
|
347
|
-
"""Hover over element by selector."""
|
|
348
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserHoverRequest
|
|
349
|
-
|
|
350
|
-
request = BrowserHoverRequest(
|
|
351
|
-
browser_session_id=session_id, selector=selector, timeout_ms=timeout_ms
|
|
352
|
-
)
|
|
353
|
-
response = await self._call_async(self._get_stub.BrowserHover, request)
|
|
354
|
-
|
|
355
|
-
if not response.success:
|
|
356
|
-
raise_browser_error(response.error, "hover", selector=selector)
|
|
357
|
-
|
|
358
|
-
async def scroll(
|
|
359
|
-
self,
|
|
360
|
-
session_id: str,
|
|
361
|
-
direction: str = "down",
|
|
362
|
-
amount: int = 500,
|
|
363
|
-
selector: str = "",
|
|
364
|
-
smooth: bool = True,
|
|
365
|
-
) -> ScrollResult:
|
|
366
|
-
"""Scroll page or element into view."""
|
|
367
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserScrollRequest
|
|
368
|
-
|
|
369
|
-
request = BrowserScrollRequest(
|
|
370
|
-
browser_session_id=session_id,
|
|
371
|
-
direction=direction,
|
|
372
|
-
amount=amount,
|
|
373
|
-
selector=selector,
|
|
374
|
-
smooth=smooth,
|
|
375
|
-
)
|
|
376
|
-
response = await self._call_async(self._get_stub.BrowserScroll, request)
|
|
377
|
-
|
|
378
|
-
if not response.success:
|
|
379
|
-
return ScrollResult(success=False, error=response.error)
|
|
380
|
-
|
|
381
|
-
return ScrollResult(
|
|
382
|
-
success=True,
|
|
383
|
-
scroll_x=response.scroll_x,
|
|
384
|
-
scroll_y=response.scroll_y,
|
|
385
|
-
scrolled_by=response.scrolled_by,
|
|
386
|
-
page_height=response.page_height,
|
|
387
|
-
viewport_height=response.viewport_height,
|
|
388
|
-
at_bottom=response.at_bottom,
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
async def get_page_info(self, session_id: str) -> PageInfo:
|
|
392
|
-
"""Get comprehensive page information."""
|
|
393
|
-
from cmdop._generated.rpc_messages.browser_pb2 import BrowserGetPageInfoRequest
|
|
394
|
-
|
|
395
|
-
request = BrowserGetPageInfoRequest(browser_session_id=session_id)
|
|
396
|
-
response = await self._call_async(self._get_stub.BrowserGetPageInfo, request)
|
|
397
|
-
|
|
398
|
-
if not response.success:
|
|
399
|
-
raise RuntimeError(f"GetPageInfo failed: {response.error}")
|
|
400
|
-
|
|
401
|
-
return PageInfo(
|
|
402
|
-
url=response.url,
|
|
403
|
-
title=response.title,
|
|
404
|
-
page_height=response.page_height,
|
|
405
|
-
viewport_height=response.viewport_height,
|
|
406
|
-
viewport_width=response.viewport_width,
|
|
407
|
-
scroll_x=response.scroll_x,
|
|
408
|
-
scroll_y=response.scroll_y,
|
|
409
|
-
at_top=response.at_top,
|
|
410
|
-
at_bottom=response.at_bottom,
|
|
411
|
-
load_time_ms=response.load_time_ms,
|
|
412
|
-
cookies_count=response.cookies_count,
|
|
413
|
-
is_https=response.is_https,
|
|
414
|
-
has_iframes=response.has_iframes,
|
|
415
|
-
dom_nodes_raw=response.dom_nodes_raw,
|
|
416
|
-
dom_nodes_cleaned=response.dom_nodes_cleaned,
|
|
417
|
-
tokens_estimate=response.tokens_estimate,
|
|
418
|
-
cloudflare_detected=response.cloudflare_detected,
|
|
419
|
-
captcha_detected=response.captcha_detected,
|
|
420
|
-
)
|