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.

Files changed (118) hide show
  1. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/PKG-INFO +2 -1
  2. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/save.py +2 -2
  3. intuned_runtime-1.3.5/intuned_runtime/captcha/__init__.py +7 -0
  4. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/pyproject.toml +2 -1
  5. intuned_runtime-1.3.5/runtime/browser/extensions/__init__.py +7 -0
  6. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/extensions/intuned_extension.py +2 -0
  7. intuned_runtime-1.3.5/runtime/browser/extensions/intuned_extension_server.py +212 -0
  8. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_chromium.py +6 -1
  9. intuned_runtime-1.3.5/runtime/helpers/extensions.py +371 -0
  10. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/settings_types.py +20 -4
  11. intuned_runtime-1.3.3/runtime/browser/extensions/__init__.py +0 -3
  12. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/LICENSE +0 -0
  13. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/README.md +0 -0
  14. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/__init__.py +0 -0
  15. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/__init__.py +0 -0
  16. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_api_command.py +0 -0
  17. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_check_command.py +0 -0
  18. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_command.py +0 -0
  19. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_authsession_create_command.py +0 -0
  20. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/attempt_command.py +0 -0
  21. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/authsession_command.py +0 -0
  22. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/authsession_record_command.py +0 -0
  23. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/command.py +0 -0
  24. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/deploy_command.py +0 -0
  25. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/init_command.py +0 -0
  26. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_api_command.py +0 -0
  27. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_command.py +0 -0
  28. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_create_command.py +0 -0
  29. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_update_command.py +0 -0
  30. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_authsession_validate_command.py +0 -0
  31. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/run_command.py +0 -0
  32. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/commands/save_command.py +0 -0
  33. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/__init__.py +0 -0
  34. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/test_api.py +0 -0
  35. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/__test__/test_authsession.py +0 -0
  36. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/api.py +0 -0
  37. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/authsession.py +0 -0
  38. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/controller/deploy.py +0 -0
  39. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/types.py +0 -0
  40. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/__test__/test_browser.py +0 -0
  41. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/__test__/test_traces.py +0 -0
  42. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/api_helpers.py +0 -0
  43. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/auth_session_helpers.py +0 -0
  44. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/backend.py +0 -0
  45. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/browser.py +0 -0
  46. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/confirmation.py +0 -0
  47. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/console.py +0 -0
  48. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/error.py +0 -0
  49. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/exclusions.py +0 -0
  50. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/get_auth_parameters.py +0 -0
  51. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/help.py +0 -0
  52. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/import_function.py +0 -0
  53. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/timeout.py +0 -0
  54. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/traces.py +0 -0
  55. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_cli/utils/wrapper.py +0 -0
  56. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/__init__.py +0 -0
  57. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/__init__.py +0 -0
  58. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/__init__.py +0 -0
  59. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/browser/save_state.py +0 -0
  60. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/__init__.py +0 -0
  61. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/__init__.py +0 -0
  62. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/check.py +0 -0
  63. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/create.py +0 -0
  64. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/auth_session/load.py +0 -0
  65. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/project.py +0 -0
  66. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/run.py +0 -0
  67. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/run_interface.py +0 -0
  68. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/project/type_check.py +0 -0
  69. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/commands/root.py +0 -0
  70. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/logger.py +0 -0
  71. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/ai_source_project.py +0 -0
  72. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/code_tree.py +0 -0
  73. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/run_apis.py +0 -0
  74. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/setup_ide_functions_token.py +0 -0
  75. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/unix_socket.py +0 -0
  76. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_internal_cli/utils/wrapper.py +0 -0
  77. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/intuned_runtime/__init__.py +0 -0
  78. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/__init__.py +0 -0
  79. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/__init__.py +0 -0
  80. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/_call_backend_function.py +0 -0
  81. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/backend_functions/get_auth_session_parameters.py +0 -0
  82. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/__init__.py +0 -0
  83. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/extensions/helpers.py +0 -0
  84. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/helpers.py +0 -0
  85. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_browser.py +0 -0
  86. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/launch_camoufox.py +0 -0
  87. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/browser/storage_state.py +0 -0
  88. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/constants.py +0 -0
  89. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/context/__init__.py +0 -0
  90. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/context/context.py +0 -0
  91. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/env.py +0 -0
  92. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/__init__.py +0 -0
  93. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/auth_session_errors.py +0 -0
  94. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/run_api_errors.py +0 -0
  95. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/errors/trace_errors.py +0 -0
  96. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/__init__.py +0 -0
  97. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/attempt_store.py +0 -0
  98. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/extend_payload.py +0 -0
  99. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/extend_timeout.py +0 -0
  100. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/helpers/get_auth_session_parameters.py +0 -0
  101. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/py.typed +0 -0
  102. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/__init__.py +0 -0
  103. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/intuned_settings.py +0 -0
  104. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/playwright_context.py +0 -0
  105. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/playwright_tracing.py +0 -0
  106. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/pydantic_encoder.py +0 -0
  107. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/run_api.py +0 -0
  108. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/setup_context_hook.py +0 -0
  109. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/traces.py +0 -0
  110. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/run/types.py +0 -0
  111. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/__init__.py +0 -0
  112. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/payload.py +0 -0
  113. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/types/run_types.py +0 -0
  114. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/__init__.py +0 -0
  115. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/anyio.py +0 -0
  116. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime/utils/config_loader.py +0 -0
  117. {intuned_runtime-1.3.3 → intuned_runtime-1.3.5}/runtime_helpers/__init__.py +0 -0
  118. {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
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) > 50:
64
- raise CLIError("Project name must be 50 characters or less.")
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):
@@ -0,0 +1,7 @@
1
+ from runtime.helpers.extensions import on_captcha_event
2
+ from runtime.helpers.extensions import wait_for_captcha_solve
3
+
4
+ __all__ = [
5
+ "on_captcha_event",
6
+ "wait_for_captcha_solve",
7
+ ]
@@ -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.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"
@@ -0,0 +1,7 @@
1
+ from .helpers import build_extensions_list
2
+ from .intuned_extension_server import clean_intuned_extension_server
3
+
4
+ __all__ = [
5
+ "build_extensions_list",
6
+ "clean_intuned_extension_server",
7
+ ]
@@ -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", remove_dir_after_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 CustomCaptchaSettings(CaptchaSettings):
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: dict[str, int | bool] = Field(
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):
@@ -1,3 +0,0 @@
1
- from .helpers import build_extensions_list
2
-
3
- __all__ = ["build_extensions_list"]
File without changes