intuned-runtime 1.3.3__tar.gz → 1.3.5__tar.gz
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.
Potentially problematic release.
This version of intuned-runtime might be problematic. Click here for more details.
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/PKG-INFO +2 -1
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/save.py +2 -2
- intuned_runtime-1.3.5/intuned_runtime/captcha/__init__.py +7 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/pyproject.toml +2 -1
- intuned_runtime-1.3.5/runtime/browser/extensions/__init__.py +7 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/extensions/intuned_extension.py +2 -0
- intuned_runtime-1.3.5/runtime/browser/extensions/intuned_extension_server.py +212 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_chromium.py +6 -1
- intuned_runtime-1.3.5/runtime/helpers/extensions.py +371 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/settings_types.py +20 -4
- intuned_runtime-1.3.3/runtime/browser/extensions/__init__.py +0 -3
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/LICENSE +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/README.md +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_api_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_check_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_create_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/authsession_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/authsession_record_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/deploy_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/init_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_api_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_create_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_update_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_validate_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/save_command.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/test_api.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/test_authsession.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/api.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/authsession.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/deploy.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/types.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/__test__/test_browser.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/__test__/test_traces.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/api_helpers.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/auth_session_helpers.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/backend.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/browser.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/confirmation.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/console.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/error.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/exclusions.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/get_auth_parameters.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/help.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/import_function.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/timeout.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/traces.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/wrapper.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/save_state.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/check.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/create.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/load.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/project.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/run.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/run_interface.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/type_check.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/root.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/logger.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/ai_source_project.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/code_tree.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/run_apis.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/setup_ide_functions_token.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/unix_socket.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/wrapper.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_runtime/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/_call_backend_function.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/get_auth_session_parameters.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/extensions/helpers.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/helpers.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_browser.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_camoufox.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/storage_state.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/constants.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/context/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/context/context.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/env.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/auth_session_errors.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/run_api_errors.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/trace_errors.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/attempt_store.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/extend_payload.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/extend_timeout.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/get_auth_session_parameters.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/py.typed +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/intuned_settings.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/playwright_context.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/playwright_tracing.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/pydantic_encoder.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/run_api.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/setup_context_hook.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/traces.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/types.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/payload.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/run_types.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/anyio.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/config_loader.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime_helpers/__init__.py +0 -0
- {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime_helpers/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intuned-runtime
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Runtime SDK that powers browser automation projects running on Intuned
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,6 +27,7 @@ Requires-Dist: jsonc-parser (>=1.1.5,<2.0.0)
|
|
|
27
27
|
Requires-Dist: more-termcolor (>=1.1.3,<2.0.0)
|
|
28
28
|
Requires-Dist: pathspec (>=0.12.1,<0.13.0)
|
|
29
29
|
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
|
30
|
+
Requires-Dist: pyee (>=13.0.0,<14.0.0)
|
|
30
31
|
Requires-Dist: pyright (>=1.1.387,<2.0.0)
|
|
31
32
|
Requires-Dist: python-dotenv (==1.0.1)
|
|
32
33
|
Requires-Dist: pytimeparse (>=1.1.8,<2.0.0)
|
|
@@ -60,8 +60,8 @@ async def validate_intuned_project():
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def validate_project_name(project_name: str):
|
|
63
|
-
if len(project_name) >
|
|
64
|
-
raise CLIError("Project name must be
|
|
63
|
+
if len(project_name) > 200:
|
|
64
|
+
raise CLIError("Project name must be 200 characters or less.")
|
|
65
65
|
|
|
66
66
|
project_name_regex = r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$"
|
|
67
67
|
if not re.match(project_name_regex, project_name):
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "intuned-runtime"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.5"
|
|
8
8
|
description = "Runtime SDK that powers browser automation projects running on Intuned"
|
|
9
9
|
authors = [ "Intuned Developers <engineering@intunedhq.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -52,6 +52,7 @@ pytimeparse = "^1.1.8"
|
|
|
52
52
|
rich = "^14.1.0"
|
|
53
53
|
jsonc-parser = "^1.1.5"
|
|
54
54
|
pyyaml = "^6.0.3"
|
|
55
|
+
pyee = "^13.0.0"
|
|
55
56
|
|
|
56
57
|
[tool.poetry.scripts]
|
|
57
58
|
intuned = "intuned_cli:run"
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/extensions/intuned_extension.py
RENAMED
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from playwright.async_api import BrowserContext
|
|
7
7
|
|
|
8
|
+
from runtime.browser.extensions.intuned_extension_server import setup_intuned_extension_server
|
|
8
9
|
from runtime.context.context import IntunedContext
|
|
9
10
|
from runtime.env import get_functions_domain
|
|
10
11
|
from runtime.env import get_project_id
|
|
@@ -70,6 +71,7 @@ async def get_intuned_extension_settings() -> dict[str, Any]:
|
|
|
70
71
|
async def setup_intuned_extension():
|
|
71
72
|
if not is_intuned_extension_enabled():
|
|
72
73
|
return
|
|
74
|
+
await setup_intuned_extension_server()
|
|
73
75
|
intuned_extension_path = get_intuned_extension_path()
|
|
74
76
|
if intuned_extension_path is None:
|
|
75
77
|
logger.warning("Intuned extension path not found, intuned extension might not work properly")
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Deque
|
|
8
|
+
from typing import Literal
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
from pyee.asyncio import AsyncIOEventEmitter
|
|
15
|
+
from waitress.server import BaseWSGIServer
|
|
16
|
+
from waitress.server import create_server
|
|
17
|
+
from waitress.server import MultiSocketServer
|
|
18
|
+
|
|
19
|
+
from runtime.types import CaptchaSolverSettings
|
|
20
|
+
from runtime.utils.config_loader import load_intuned_json
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(level=logging.INFO)
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CaptchaEvent = Literal[
|
|
27
|
+
"CAPTCHA_EXTENSION_READY", "CAPTCHA_DETECTED", "CAPTCHA_SOLVED", "HIT_LIMIT", "MAX_RETRIES_EXHAUSTED", "ERROR"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EventRequest(BaseModel):
|
|
32
|
+
model_config = {"populate_by_name": True}
|
|
33
|
+
event: CaptchaEvent
|
|
34
|
+
# tab_id: str = Field(None, alias="tabId")
|
|
35
|
+
session_id: Optional[str] = Field(None, alias="sessionId")
|
|
36
|
+
payload: Optional[Any] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EventQueues:
|
|
40
|
+
queues: dict[CaptchaEvent, Deque[EventRequest]]
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.queues = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TabEventQueue:
|
|
47
|
+
events_queue: dict[str, EventQueues]
|
|
48
|
+
last_detection_event: Optional[EventRequest]
|
|
49
|
+
|
|
50
|
+
def __init__(self, tab_id: str):
|
|
51
|
+
self.tab_id = tab_id
|
|
52
|
+
self.events_queue = {}
|
|
53
|
+
self.last_detection_event = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ExtensionServer:
|
|
57
|
+
tabs: dict[str, TabEventQueue]
|
|
58
|
+
is_healthy: bool = False
|
|
59
|
+
_server: Optional[MultiSocketServer | BaseWSGIServer] = None
|
|
60
|
+
_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
61
|
+
_thread: Optional[threading.Thread] = None
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.tabs = dict()
|
|
65
|
+
|
|
66
|
+
def __call__(self, environ, start_response):
|
|
67
|
+
"""WSGI application"""
|
|
68
|
+
path = environ.get("PATH_INFO", "")
|
|
69
|
+
method = environ["REQUEST_METHOD"]
|
|
70
|
+
|
|
71
|
+
if path == "/ingest" and method == "POST":
|
|
72
|
+
return self._handle_ingest(environ, start_response)
|
|
73
|
+
|
|
74
|
+
start_response("404 Not Found", [("Content-Type", "application/json")])
|
|
75
|
+
return [json.dumps({"error": "Not found"}).encode()]
|
|
76
|
+
|
|
77
|
+
def _handle_queue_event(self, event: EventRequest):
|
|
78
|
+
queueable_events: list[CaptchaEvent] = [
|
|
79
|
+
"CAPTCHA_DETECTED",
|
|
80
|
+
"CAPTCHA_SOLVED",
|
|
81
|
+
"HIT_LIMIT",
|
|
82
|
+
"MAX_RETRIES_EXHAUSTED",
|
|
83
|
+
"ERROR",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if event.event not in queueable_events:
|
|
87
|
+
return
|
|
88
|
+
tab_id = "page-0" # We will revisit on multi-tab support
|
|
89
|
+
if event.session_id is None:
|
|
90
|
+
return
|
|
91
|
+
if tab_id not in self.tabs:
|
|
92
|
+
self.tabs[tab_id] = TabEventQueue(tab_id=tab_id)
|
|
93
|
+
tab_info = self.tabs[tab_id]
|
|
94
|
+
if event.event == "CAPTCHA_DETECTED":
|
|
95
|
+
tab_info.last_detection_event = event
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if event.session_id not in tab_info.events_queue:
|
|
99
|
+
tab_info.events_queue[event.session_id] = EventQueues()
|
|
100
|
+
|
|
101
|
+
event_queues = tab_info.events_queue[event.session_id]
|
|
102
|
+
if event.event not in event_queues.queues:
|
|
103
|
+
event_queues.queues[event.event] = deque(maxlen=5)
|
|
104
|
+
|
|
105
|
+
event_queues.queues[event.event].append(event)
|
|
106
|
+
|
|
107
|
+
def _handle_ingest(self, environ, start_response):
|
|
108
|
+
try:
|
|
109
|
+
global event_emitter
|
|
110
|
+
if event_emitter is None:
|
|
111
|
+
event_emitter = AsyncIOEventEmitter()
|
|
112
|
+
content_length = int(environ.get("CONTENT_LENGTH", 0))
|
|
113
|
+
body = environ["wsgi.input"].read(content_length)
|
|
114
|
+
data = json.loads(body)
|
|
115
|
+
event_data = EventRequest(**data)
|
|
116
|
+
if event_data.event == "CAPTCHA_EXTENSION_READY":
|
|
117
|
+
self.is_healthy = True
|
|
118
|
+
self._handle_queue_event(event=event_data)
|
|
119
|
+
if self._loop and not self._loop.is_closed():
|
|
120
|
+
self._loop.call_soon_threadsafe(event_emitter.emit, event_data.event, {})
|
|
121
|
+
start_response("200 OK", [("Content-Type", "application/json")])
|
|
122
|
+
return [json.dumps({}).encode()]
|
|
123
|
+
|
|
124
|
+
except ValidationError as e:
|
|
125
|
+
start_response("400 Bad Request", [("Content-Type", "application/json")])
|
|
126
|
+
return [json.dumps({"error": e.errors()}).encode()]
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Error: {e}")
|
|
129
|
+
start_response("500 Internal Server Error", [("Content-Type", "application/json")])
|
|
130
|
+
return [json.dumps({"error": "Internal server error"}).encode()]
|
|
131
|
+
|
|
132
|
+
async def start(self, port: int = 3000, host: str = "0.0.0.0") -> None:
|
|
133
|
+
"""Start server using daemon thread"""
|
|
134
|
+
self._loop = asyncio.get_running_loop()
|
|
135
|
+
self._server = create_server(self.__call__, host=host, port=port)
|
|
136
|
+
|
|
137
|
+
def _run_server():
|
|
138
|
+
try:
|
|
139
|
+
if self._server:
|
|
140
|
+
self._server.run()
|
|
141
|
+
except OSError as err:
|
|
142
|
+
if err.errno != 9:
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
self._thread = threading.Thread(target=_run_server, daemon=True)
|
|
146
|
+
self._thread.start()
|
|
147
|
+
|
|
148
|
+
async def stop(self):
|
|
149
|
+
self._loop = None
|
|
150
|
+
if self._server:
|
|
151
|
+
self._server.close()
|
|
152
|
+
|
|
153
|
+
if self._thread and self._thread.is_alive():
|
|
154
|
+
self._thread.join(timeout=5.0)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
event_emitter: Optional[AsyncIOEventEmitter] = None
|
|
158
|
+
extension_server: Optional[ExtensionServer] = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def setup_intuned_extension_server():
|
|
162
|
+
global event_emitter, extension_server
|
|
163
|
+
intuned_json = await load_intuned_json()
|
|
164
|
+
captcha_settings: CaptchaSolverSettings = (
|
|
165
|
+
intuned_json.captcha_solver
|
|
166
|
+
if intuned_json and intuned_json.captcha_solver is not None
|
|
167
|
+
else CaptchaSolverSettings()
|
|
168
|
+
)
|
|
169
|
+
extension_server = ExtensionServer()
|
|
170
|
+
event_emitter = AsyncIOEventEmitter()
|
|
171
|
+
await extension_server.start(port=captcha_settings.port)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def clean_intuned_extension_server():
|
|
175
|
+
global event_emitter, extension_server
|
|
176
|
+
if extension_server is not None:
|
|
177
|
+
await extension_server.stop()
|
|
178
|
+
extension_server = None
|
|
179
|
+
|
|
180
|
+
if event_emitter is not None:
|
|
181
|
+
event_emitter.remove_all_listeners()
|
|
182
|
+
event_emitter = None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_event_from_event_queue(
|
|
186
|
+
event: CaptchaEvent, tab_id: str = "page-0", session_id: str = ""
|
|
187
|
+
) -> Optional[EventRequest]:
|
|
188
|
+
if extension_server is None:
|
|
189
|
+
raise RuntimeError("Extension server is not initialized or healthy")
|
|
190
|
+
|
|
191
|
+
tab_info = extension_server.tabs.get(tab_id)
|
|
192
|
+
if tab_info is None:
|
|
193
|
+
return None
|
|
194
|
+
if event == "CAPTCHA_DETECTED":
|
|
195
|
+
value = tab_info.last_detection_event
|
|
196
|
+
tab_info.last_detection_event = None
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
if session_id not in tab_info.events_queue:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
event_queues = tab_info.events_queue[session_id]
|
|
203
|
+
if event not in event_queues.queues or len(event_queues.queues[event]) == 0:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
return event_queues.queues[event].popleft()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_intuned_event_emitter() -> AsyncIOEventEmitter:
|
|
210
|
+
if event_emitter is None:
|
|
211
|
+
raise RuntimeError("Event emitter is not initliazed")
|
|
212
|
+
return event_emitter
|
|
@@ -12,6 +12,7 @@ from runtime.browser.extensions import build_extensions_list
|
|
|
12
12
|
from runtime.browser.extensions.intuned_extension import get_intuned_worker
|
|
13
13
|
from runtime.browser.extensions.intuned_extension import is_intuned_extension_enabled
|
|
14
14
|
from runtime.browser.extensions.intuned_extension import setup_intuned_extension
|
|
15
|
+
from runtime.browser.extensions.intuned_extension_server import clean_intuned_extension_server
|
|
15
16
|
|
|
16
17
|
from .helpers import get_local_cdp_address
|
|
17
18
|
from .helpers import get_proxy_env
|
|
@@ -127,6 +128,10 @@ async def launch_chromium(
|
|
|
127
128
|
|
|
128
129
|
context.set_default_timeout(timeout * 1000)
|
|
129
130
|
|
|
131
|
+
async def clean_up_after_close(*_: Any, **__: Any) -> None:
|
|
132
|
+
await remove_dir_after_close()
|
|
133
|
+
await clean_intuned_extension_server()
|
|
134
|
+
|
|
130
135
|
async def remove_dir_after_close(*_: Any, **__: Any) -> None:
|
|
131
136
|
if not dir_to_clean:
|
|
132
137
|
return
|
|
@@ -142,7 +147,7 @@ async def launch_chromium(
|
|
|
142
147
|
)
|
|
143
148
|
await process.wait()
|
|
144
149
|
|
|
145
|
-
context.once("close",
|
|
150
|
+
context.once("close", clean_up_after_close)
|
|
146
151
|
|
|
147
152
|
yield context, context.pages[0]
|
|
148
153
|
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Awaitable
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from typing import overload
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
from playwright.async_api import Page
|
|
12
|
+
|
|
13
|
+
from runtime.browser.extensions.intuned_extension_server import CaptchaEvent
|
|
14
|
+
from runtime.browser.extensions.intuned_extension_server import EventRequest
|
|
15
|
+
from runtime.browser.extensions.intuned_extension_server import get_event_from_event_queue
|
|
16
|
+
from runtime.browser.extensions.intuned_extension_server import get_intuned_event_emitter
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Overload 1: Direct call with page only (callable pattern)
|
|
24
|
+
@overload
|
|
25
|
+
async def wait_for_captcha_solve(
|
|
26
|
+
page: Page,
|
|
27
|
+
*,
|
|
28
|
+
timeout: int = 10_000,
|
|
29
|
+
) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Overload 2: Wrapper pattern with page and func
|
|
33
|
+
@overload
|
|
34
|
+
async def wait_for_captcha_solve(
|
|
35
|
+
*,
|
|
36
|
+
page: Page,
|
|
37
|
+
func: Callable[[], Awaitable[Any]],
|
|
38
|
+
timeout: int = 10_000,
|
|
39
|
+
wait_for_network_settled: bool = True,
|
|
40
|
+
) -> Any: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Overload 3: Decorator without arguments
|
|
44
|
+
@overload
|
|
45
|
+
def wait_for_captcha_solve(
|
|
46
|
+
func: Callable[..., Awaitable[Any]],
|
|
47
|
+
) -> Callable[..., Awaitable[Any]]: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Overload 4: Decorator factory with arguments
|
|
51
|
+
@overload
|
|
52
|
+
def wait_for_captcha_solve(
|
|
53
|
+
*,
|
|
54
|
+
timeout: int = 10_000,
|
|
55
|
+
wait_for_network_settled: bool = True,
|
|
56
|
+
) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def wait_for_captcha_solve(
|
|
60
|
+
*args: Any,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> Any:
|
|
63
|
+
"""
|
|
64
|
+
Wait for CAPTCHA solve after performing an action or by itself.
|
|
65
|
+
|
|
66
|
+
Usage patterns:
|
|
67
|
+
1. Callable: await wait_for_captcha_solve(page, timeout=10_000)
|
|
68
|
+
2. Wrapper: await wait_for_captcha_solve(page=page, func=my_func, timeout=10_000)
|
|
69
|
+
3. Decorator: @wait_for_captcha_solve or @wait_for_captcha_solve()
|
|
70
|
+
4. Decorator with options: @wait_for_captcha_solve(timeout=10_000, wait_for_network_settled=True)
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
page: Playwright Page object
|
|
74
|
+
func: Optional callable to execute before waiting for captcha solve
|
|
75
|
+
timeout: Maximum time to wait in milliseconds (default: 10_000)
|
|
76
|
+
wait_for_network_settled: Whether to wait for network idle before checking captcha (default: True)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Case 1a: Direct call with page only (callable pattern - positional)
|
|
80
|
+
# await wait_for_captcha_solve(page, timeout=10_000, wait_for_network_settled=True)
|
|
81
|
+
if len(args) == 1 and isinstance(args[0], Page):
|
|
82
|
+
page = args[0]
|
|
83
|
+
timeout = kwargs.get("timeout", 10_000)
|
|
84
|
+
wait_for_network_settled = kwargs.get("wait_for_network_settled", True)
|
|
85
|
+
return _wait_for_captcha_solve_core(
|
|
86
|
+
page=page,
|
|
87
|
+
func=None,
|
|
88
|
+
timeout=timeout,
|
|
89
|
+
wait_for_network_settled=wait_for_network_settled,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Case 1b: Direct call with page only (callable pattern - keyword)
|
|
93
|
+
# await wait_for_captcha_solve(page=page, timeout=10_000, wait_for_network_settled=True)
|
|
94
|
+
if "page" in kwargs and "func" not in kwargs and len(args) == 0:
|
|
95
|
+
page = kwargs["page"]
|
|
96
|
+
timeout = kwargs.get("timeout", 10_000)
|
|
97
|
+
wait_for_network_settled = kwargs.get("wait_for_network_settled", True)
|
|
98
|
+
|
|
99
|
+
if not isinstance(page, Page):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"No Page object found in function arguments. 'page' parameter must be a Playwright Page object."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return _wait_for_captcha_solve_core(
|
|
105
|
+
page=page,
|
|
106
|
+
func=None,
|
|
107
|
+
timeout=timeout,
|
|
108
|
+
wait_for_network_settled=wait_for_network_settled,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Case 2: Wrapper pattern with page and func as keyword arguments
|
|
112
|
+
# await wait_for_captcha_solve(page=page, func=func, timeout=10_000)
|
|
113
|
+
if "page" in kwargs and "func" in kwargs:
|
|
114
|
+
page = kwargs["page"]
|
|
115
|
+
func = kwargs["func"]
|
|
116
|
+
timeout = kwargs.get("timeout", 10_000)
|
|
117
|
+
wait_for_network_settled = kwargs.get("wait_for_network_settled", True)
|
|
118
|
+
|
|
119
|
+
if not isinstance(page, Page):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"No Page object found in function arguments. 'page' parameter must be a Playwright Page object."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return _wait_for_captcha_solve_core(
|
|
125
|
+
page=page,
|
|
126
|
+
func=func,
|
|
127
|
+
timeout=timeout,
|
|
128
|
+
wait_for_network_settled=wait_for_network_settled,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Case 3: Decorator without arguments
|
|
132
|
+
# @wait_for_captcha_solve
|
|
133
|
+
if len(args) == 1 and callable(args[0]) and not isinstance(args[0], Page):
|
|
134
|
+
func = args[0]
|
|
135
|
+
return _create_decorated_function(func, timeout=10_000, wait_for_network_settled=True) # type: ignore
|
|
136
|
+
|
|
137
|
+
# Case 4: Decorator factory with arguments (including empty parentheses)
|
|
138
|
+
# @wait_for_captcha_solve() or @wait_for_captcha_solve(timeout=10_000, wait_for_network_settled=True)
|
|
139
|
+
if len(args) == 0 and "page" not in kwargs and "func" not in kwargs:
|
|
140
|
+
timeout = kwargs.get("timeout", 10_000)
|
|
141
|
+
wait_for_network_settled = kwargs.get("wait_for_network_settled", True)
|
|
142
|
+
|
|
143
|
+
def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
144
|
+
return _create_decorated_function(func, timeout=timeout, wait_for_network_settled=wait_for_network_settled)
|
|
145
|
+
|
|
146
|
+
return decorator
|
|
147
|
+
|
|
148
|
+
raise ValueError(
|
|
149
|
+
"Invalid usage. Valid patterns:\n"
|
|
150
|
+
"1. await wait_for_captcha_solve(page, timeout=10_000) or await wait_for_captcha_solve(page=page, timeout=10_000)\n"
|
|
151
|
+
"2. await wait_for_captcha_solve(page=page, func=func, timeout=10_000)\n"
|
|
152
|
+
"3. @wait_for_captcha_solve or @wait_for_captcha_solve()\n"
|
|
153
|
+
"4. @wait_for_captcha_solve(timeout=10_000, wait_for_network_settled=True)"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _create_decorated_function(
|
|
158
|
+
func: Callable[..., Awaitable[Any]],
|
|
159
|
+
timeout: int,
|
|
160
|
+
wait_for_network_settled: bool,
|
|
161
|
+
) -> Callable[..., Awaitable[Any]]:
|
|
162
|
+
"""Helper to create a decorated function with captcha solve waiting."""
|
|
163
|
+
|
|
164
|
+
@functools.wraps(func)
|
|
165
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
166
|
+
# Find the page object in function arguments
|
|
167
|
+
page = next((arg for arg in args if isinstance(arg, Page)), None)
|
|
168
|
+
if page is None:
|
|
169
|
+
page = kwargs.get("page")
|
|
170
|
+
|
|
171
|
+
if not page or not isinstance(page, Page):
|
|
172
|
+
logger.error(
|
|
173
|
+
"No Page object found in function arguments. The decorated function must have a 'page' parameter or receive a Page object as an argument."
|
|
174
|
+
)
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"No Page object found in function arguments. The decorated function must have a 'page' parameter or receive a Page object as an argument."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def func_with_args():
|
|
180
|
+
return await func(*args, **kwargs)
|
|
181
|
+
|
|
182
|
+
return await _wait_for_captcha_solve_core(
|
|
183
|
+
page=page,
|
|
184
|
+
func=func_with_args,
|
|
185
|
+
timeout=timeout,
|
|
186
|
+
wait_for_network_settled=wait_for_network_settled,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return wrapper
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def _wait_for_captcha_solve_core(
|
|
193
|
+
*,
|
|
194
|
+
page: Page,
|
|
195
|
+
func: Optional[Callable[..., Awaitable[Any]]],
|
|
196
|
+
timeout: int = 10_000,
|
|
197
|
+
wait_for_network_settled: bool = True,
|
|
198
|
+
):
|
|
199
|
+
"""Core implementation of captcha solve waiting logic."""
|
|
200
|
+
if not isinstance(page, Page):
|
|
201
|
+
raise ValueError("No Page object found in function arguments. Page parameter must be a Playwright Page object.")
|
|
202
|
+
|
|
203
|
+
logger.debug(f"Page object: {page}")
|
|
204
|
+
|
|
205
|
+
# Execute function if provided
|
|
206
|
+
result = None
|
|
207
|
+
if func is not None:
|
|
208
|
+
result = await func()
|
|
209
|
+
|
|
210
|
+
# Wait for network to settle if requested
|
|
211
|
+
if wait_for_network_settled:
|
|
212
|
+
try:
|
|
213
|
+
await page.wait_for_load_state("networkidle", timeout=timeout)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.debug(f"Network idle wait failed: {e}")
|
|
216
|
+
|
|
217
|
+
# Check for captcha detection
|
|
218
|
+
detection_event: EventRequest
|
|
219
|
+
try:
|
|
220
|
+
detection_event = await wait_for_captcha_event("CAPTCHA_DETECTED", timeout=timeout)
|
|
221
|
+
except:
|
|
222
|
+
logger.debug("CAPTCHA Not detected, skipping...")
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
logger.info("CAPTCHA Detected, awaiting result...")
|
|
226
|
+
if detection_event.session_id is None:
|
|
227
|
+
raise RuntimeError("CAPTCHA_DETECTED event missing session ID")
|
|
228
|
+
try:
|
|
229
|
+
solved_task = asyncio.create_task(
|
|
230
|
+
wait_for_captcha_event("CAPTCHA_SOLVED", session_id=detection_event.session_id, timeout=timeout)
|
|
231
|
+
)
|
|
232
|
+
max_retries_task = asyncio.create_task(
|
|
233
|
+
wait_for_captcha_event("MAX_RETRIES_EXHAUSTED", session_id=detection_event.session_id, timeout=timeout)
|
|
234
|
+
)
|
|
235
|
+
error_task = asyncio.create_task(
|
|
236
|
+
wait_for_captcha_event("ERROR", session_id=detection_event.session_id, timeout=timeout)
|
|
237
|
+
)
|
|
238
|
+
hit_limit_task = asyncio.create_task(
|
|
239
|
+
wait_for_captcha_event("HIT_LIMIT", session_id=detection_event.session_id, timeout=timeout)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
done, pending = await asyncio.wait(
|
|
243
|
+
[solved_task, max_retries_task, error_task, hit_limit_task], return_when=asyncio.FIRST_COMPLETED
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Cancel pending tasks
|
|
247
|
+
for task in pending:
|
|
248
|
+
task.cancel()
|
|
249
|
+
|
|
250
|
+
# Get the completed task
|
|
251
|
+
completed = done.pop()
|
|
252
|
+
exception = completed.exception()
|
|
253
|
+
if exception:
|
|
254
|
+
raise exception
|
|
255
|
+
|
|
256
|
+
# Check which task completed
|
|
257
|
+
if completed == max_retries_task:
|
|
258
|
+
raise RuntimeError("Reached maximum retries on solving captcha")
|
|
259
|
+
elif completed == error_task:
|
|
260
|
+
raise RuntimeError("Captcha error")
|
|
261
|
+
elif completed == hit_limit_task:
|
|
262
|
+
raise RuntimeError("Insufficient resource credits to execute the captcha solve")
|
|
263
|
+
|
|
264
|
+
logger.info("CAPTCHA solved successfully")
|
|
265
|
+
|
|
266
|
+
except asyncio.TimeoutError:
|
|
267
|
+
logger.error("CAPTCHA Result timeout")
|
|
268
|
+
raise
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"CAPTCHA solve error: {e}")
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def on_captcha_event(
|
|
277
|
+
event: CaptchaEvent,
|
|
278
|
+
f: Callable[..., Awaitable[None] | None],
|
|
279
|
+
*args,
|
|
280
|
+
**kwargs,
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
Register a callback for a captcha event.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
event: The captcha event to listen for
|
|
287
|
+
f: The callback function to execute
|
|
288
|
+
*args: Additional arguments to pass to the callback
|
|
289
|
+
**kwargs: Additional keyword arguments to pass to the callback
|
|
290
|
+
"""
|
|
291
|
+
emitter = get_intuned_event_emitter()
|
|
292
|
+
|
|
293
|
+
async def wrapper(*_, **__):
|
|
294
|
+
result = f(*args, **kwargs)
|
|
295
|
+
if asyncio.iscoroutine(result):
|
|
296
|
+
await result
|
|
297
|
+
|
|
298
|
+
emitter.on(event, wrapper)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def once_captcha_event(
|
|
302
|
+
event: CaptchaEvent,
|
|
303
|
+
f: Callable[..., Awaitable[None] | None],
|
|
304
|
+
*args,
|
|
305
|
+
**kwargs,
|
|
306
|
+
):
|
|
307
|
+
"""
|
|
308
|
+
Register a one-time callback for a captcha event.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
event: The captcha event to listen for
|
|
312
|
+
f: The callback function to execute
|
|
313
|
+
*args: Additional arguments to pass to the callback
|
|
314
|
+
**kwargs: Additional keyword arguments to pass to the callback
|
|
315
|
+
"""
|
|
316
|
+
event_emitter = get_intuned_event_emitter()
|
|
317
|
+
|
|
318
|
+
async def wrapper(*_, **__):
|
|
319
|
+
result = f(*args, **kwargs)
|
|
320
|
+
if asyncio.iscoroutine(result):
|
|
321
|
+
await result
|
|
322
|
+
|
|
323
|
+
event_emitter.once(event, wrapper)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def wait_for_captcha_event(event: CaptchaEvent, session_id: str = "", timeout: int = 10_000) -> EventRequest:
|
|
327
|
+
"""
|
|
328
|
+
Wait for a captcha event to be emitted.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
event: Event name to wait for
|
|
332
|
+
session_id: ID for the captcha solve session
|
|
333
|
+
timeout: Optional timeout in milliseconds (default: 10_000)
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
The event payload
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
RuntimeError: If the event emitter is not initialized
|
|
340
|
+
asyncio.TimeoutError: If the timeout is reached before the event fires
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
# Check if event was already fired before attaching the listener
|
|
344
|
+
event_from_queue = get_event_from_event_queue(
|
|
345
|
+
event=event, tab_id="page-0", session_id=session_id
|
|
346
|
+
) # For now we stick with page-0, will revisit on multi page support
|
|
347
|
+
if event_from_queue is not None:
|
|
348
|
+
return event_from_queue
|
|
349
|
+
|
|
350
|
+
emitter = get_intuned_event_emitter()
|
|
351
|
+
if emitter is None:
|
|
352
|
+
raise RuntimeError("Intuned Extensions listener is not initialized")
|
|
353
|
+
|
|
354
|
+
loop = asyncio.get_event_loop()
|
|
355
|
+
future: asyncio.Future = asyncio.Future()
|
|
356
|
+
|
|
357
|
+
def handler(*_, **__):
|
|
358
|
+
if not future.done():
|
|
359
|
+
consumed_event = get_event_from_event_queue(session_id=session_id, event=event)
|
|
360
|
+
loop.call_soon_threadsafe(future.set_result, consumed_event)
|
|
361
|
+
|
|
362
|
+
emitter.once(event, handler)
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
if timeout:
|
|
366
|
+
return await asyncio.wait_for(future, timeout=timeout / 1000)
|
|
367
|
+
else:
|
|
368
|
+
return await future
|
|
369
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
370
|
+
emitter.remove_listener(event, handler)
|
|
371
|
+
raise
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import socket
|
|
1
2
|
from typing import List
|
|
2
3
|
from typing import Literal
|
|
3
4
|
|
|
@@ -5,15 +6,31 @@ from pydantic import BaseModel
|
|
|
5
6
|
from pydantic import Field
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
def get_random_port():
|
|
10
|
+
with socket.socket() as s:
|
|
11
|
+
s.bind(("", 0))
|
|
12
|
+
return s.getsockname()[1]
|
|
13
|
+
|
|
14
|
+
|
|
8
15
|
class CaptchaSettings(BaseModel):
|
|
9
16
|
enabled: bool = Field(default=False)
|
|
10
17
|
|
|
11
18
|
|
|
12
|
-
class
|
|
19
|
+
class CaptchaSolverSolveSettings(BaseModel):
|
|
13
20
|
model_config = {
|
|
14
21
|
"populate_by_name": True,
|
|
15
22
|
"serialize_by_alias": True,
|
|
16
23
|
}
|
|
24
|
+
auto_solve: bool = Field(default=True, alias="autoSolve")
|
|
25
|
+
solve_delay: int = Field(default=2000, alias="solveDelay")
|
|
26
|
+
max_retries: int = Field(default=3, alias="maxRetries")
|
|
27
|
+
timeout: int = Field(default=30000)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CustomCaptchaSettings(CaptchaSettings):
|
|
31
|
+
model_config = {
|
|
32
|
+
"serialize_by_alias": True,
|
|
33
|
+
}
|
|
17
34
|
|
|
18
35
|
image_locators: List[str] = Field(alias="imageLocators", default=[])
|
|
19
36
|
submit_locators: List[str] = Field(alias="submitLocators", default=[])
|
|
@@ -37,6 +54,7 @@ class CaptchaSolverSettings(BaseModel):
|
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
enabled: bool = Field(default=False)
|
|
57
|
+
port: int = Field(default_factory=get_random_port)
|
|
40
58
|
cloudflare: CaptchaSettings = Field(default_factory=CaptchaSettings)
|
|
41
59
|
google_recaptcha_v2: CaptchaSettings = Field(alias="googleRecaptchaV2", default_factory=CaptchaSettings)
|
|
42
60
|
google_recaptcha_v3: CaptchaSettings = Field(alias="googleRecaptchaV3", default_factory=CaptchaSettings)
|
|
@@ -47,9 +65,7 @@ class CaptchaSolverSettings(BaseModel):
|
|
|
47
65
|
lemin: CaptchaSettings = Field(default_factory=CaptchaSettings)
|
|
48
66
|
custom_captcha: CustomCaptchaSettings = Field(alias="customCaptcha", default_factory=CustomCaptchaSettings)
|
|
49
67
|
text: TextCaptchaSettings = Field(default_factory=TextCaptchaSettings)
|
|
50
|
-
settings:
|
|
51
|
-
default={"autoSolve": True, "solveDelay": 2000, "maxRetries": 3, "timeout": 30000}
|
|
52
|
-
)
|
|
68
|
+
settings: CaptchaSolverSolveSettings = Field(default_factory=CaptchaSolverSolveSettings)
|
|
53
69
|
|
|
54
70
|
|
|
55
71
|
class IntunedJsonDisabledAuthSessions(BaseModel):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/authsession_record_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/test_authsession.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/__init__.py
RENAMED
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/save_state.py
RENAMED
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/project.py
RENAMED
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/run.py
RENAMED
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/type_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/ai_source_project.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/_call_backend_function.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/get_auth_session_parameters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|