intuned-runtime 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. intuned_cli/__init__.py +12 -2
  2. intuned_cli/commands/__init__.py +1 -0
  3. intuned_cli/commands/attempt_api_command.py +1 -1
  4. intuned_cli/commands/attempt_authsession_check_command.py +1 -1
  5. intuned_cli/commands/attempt_authsession_create_command.py +1 -1
  6. intuned_cli/commands/deploy_command.py +4 -4
  7. intuned_cli/commands/run_api_command.py +1 -1
  8. intuned_cli/commands/run_authsession_create_command.py +1 -1
  9. intuned_cli/commands/run_authsession_update_command.py +1 -1
  10. intuned_cli/commands/run_authsession_validate_command.py +1 -1
  11. intuned_cli/commands/save_command.py +47 -0
  12. intuned_cli/controller/__test__/test_api.py +0 -1
  13. intuned_cli/controller/api.py +0 -1
  14. intuned_cli/controller/authsession.py +0 -1
  15. intuned_cli/controller/deploy.py +8 -210
  16. intuned_cli/controller/save.py +260 -0
  17. intuned_cli/utils/backend.py +26 -0
  18. intuned_cli/utils/error.py +7 -3
  19. intuned_internal_cli/commands/project/auth_session/check.py +0 -1
  20. intuned_internal_cli/commands/project/run.py +0 -2
  21. intuned_runtime/__init__.py +2 -1
  22. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/METADATA +4 -2
  23. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/RECORD +43 -36
  24. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/WHEEL +1 -1
  25. runtime/backend_functions/_call_backend_function.py +18 -2
  26. runtime/browser/helpers.py +22 -0
  27. runtime/browser/launch_browser.py +43 -15
  28. runtime/browser/launch_chromium.py +27 -57
  29. runtime/constants.py +1 -0
  30. runtime/context/context.py +1 -0
  31. runtime/env.py +15 -1
  32. runtime/errors/run_api_errors.py +8 -0
  33. runtime/helpers/__init__.py +2 -1
  34. runtime/helpers/attempt_store.py +14 -0
  35. runtime/run/playwright_context.py +147 -0
  36. runtime/run/playwright_tracing.py +27 -0
  37. runtime/run/run_api.py +71 -91
  38. runtime/run/setup_context_hook.py +40 -0
  39. runtime/run/types.py +14 -0
  40. runtime/types/run_types.py +0 -1
  41. runtime_helpers/__init__.py +2 -1
  42. runtime/run/playwright_constructs.py +0 -20
  43. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/entry_points.txt +0 -0
  44. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info/licenses}/LICENSE +0 -0
@@ -1,31 +1,59 @@
1
1
  from contextlib import asynccontextmanager
2
+ from contextlib import AsyncExitStack
3
+ from typing import AsyncContextManager
4
+ from typing import overload
5
+ from typing import TYPE_CHECKING
2
6
 
3
- from playwright.async_api import ProxySettings
7
+ if TYPE_CHECKING:
8
+ from playwright.async_api import BrowserContext
9
+ from playwright.async_api import Page
10
+ from playwright.async_api import ProxySettings
4
11
 
5
12
  from runtime.env import get_browser_type
13
+ from runtime.errors.run_api_errors import AutomationError
6
14
 
7
15
  from .launch_camoufox import launch_camoufox
8
16
  from .launch_chromium import launch_chromium
9
17
 
10
18
 
19
+ @overload
20
+ def launch_browser(
21
+ *,
22
+ cdp_address: str,
23
+ ) -> "AsyncContextManager[tuple['BrowserContext', 'Page']]": ...
24
+
25
+
26
+ @overload
27
+ def launch_browser(
28
+ proxy: "ProxySettings | None" = None,
29
+ headless: bool = False,
30
+ *,
31
+ cdp_port: int | None = None,
32
+ ) -> "AsyncContextManager[tuple['BrowserContext', 'Page']]": ...
33
+
34
+
11
35
  @asynccontextmanager
12
36
  async def launch_browser(
13
- proxy: ProxySettings | None = None,
37
+ proxy: "ProxySettings | None" = None,
14
38
  headless: bool = False,
15
39
  *,
40
+ cdp_port: int | None = None,
16
41
  cdp_address: str | None = None,
17
42
  ):
18
43
  browser_type = get_browser_type()
19
- match browser_type:
20
- case "camoufox":
21
- async with launch_camoufox(headless=headless, proxy=proxy) as (context, page):
22
- try:
23
- yield context, page
24
- finally:
25
- await context.close()
26
- case "chromium" | _:
27
- async with launch_chromium(headless=headless, cdp_address=cdp_address, proxy=proxy) as (context, page):
28
- try:
29
- yield context, page
30
- finally:
31
- await context.close()
44
+ async with AsyncExitStack() as stack:
45
+ match browser_type:
46
+ case "camoufox":
47
+ if cdp_address:
48
+ raise AutomationError(ValueError("CDP address is not supported with Camoufox"))
49
+ if cdp_port:
50
+ raise AutomationError(ValueError("CDP port is not supported with Camoufox"))
51
+ context, page = await stack.enter_async_context(launch_camoufox(headless=headless, proxy=proxy))
52
+ case "chromium" | _:
53
+ context, page = await stack.enter_async_context(
54
+ launch_chromium(headless=headless, cdp_address=cdp_address, cdp_port=cdp_port, proxy=proxy)
55
+ )
56
+ try:
57
+ yield context, page
58
+ finally:
59
+ await context.close()
@@ -4,46 +4,19 @@ import logging
4
4
  import os
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import Any
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  import anyio
9
10
 
11
+ from .helpers import get_local_cdp_address
10
12
  from .helpers import get_proxy_env
13
+ from .helpers import wait_on_cdp_address
11
14
 
12
15
  logger = logging.getLogger(__name__)
13
16
 
14
-
15
- chromium_launch_args_to_ignore = [
16
- "--disable-field-trial-config",
17
- "--disable-background-networking",
18
- "--enable-features=NetworkService,NetworkServiceInProcess",
19
- "--disable-background-timer-throttling",
20
- "--disable-backgrounding-occluded-windows",
21
- "--disable-back-forward-cache",
22
- "--disable-breakpad",
23
- "--disable-client-side-phishing-detection",
24
- "--disable-component-extensions-with-background-pages",
25
- "--disable-component-update",
26
- "--no-default-browser-check",
27
- "--disable-default-apps",
28
- "--disable-dev-shm-usage",
29
- "--disable-extensions",
30
- "--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,TranslateUI",
31
- "--allow-pre-commit-input",
32
- "--disable-hang-monitor",
33
- "--disable-ipc-flooding-protection",
34
- "--disable-prompt-on-repost",
35
- "--disable-renderer-backgrounding",
36
- "--force-color-profile=srgb",
37
- "--metrics-recording-only",
38
- "--no-first-run",
39
- "--enable-automation",
40
- "--password-store=basic",
41
- "--use-mock-keychain",
42
- "--no-service-autorun",
43
- "--export-tagged-pdf",
44
- "--enable-use-zoom-for-dsf=false",
45
- "--disable-popup-blocking",
46
- ]
17
+ if TYPE_CHECKING:
18
+ from playwright.async_api import ProxySettings
19
+ from playwright.async_api import ViewportSize
47
20
 
48
21
 
49
22
  async def create_user_dir_with_preferences():
@@ -78,21 +51,17 @@ default_user_agent = (
78
51
  async def launch_chromium(
79
52
  headless: bool = True,
80
53
  timeout: int = 10,
54
+ cdp_port: int | None = None,
81
55
  cdp_address: str | None = None,
56
+ *,
57
+ proxy: "ProxySettings | None" = None,
58
+ viewport: "ViewportSize | None" = None,
82
59
  **kwargs: Any,
83
60
  ):
84
61
  from playwright.async_api import async_playwright
85
62
  from playwright.async_api import Browser
86
63
 
87
- extra_args = [
88
- "--no-first-run",
89
- "--disable-sync",
90
- "--disable-translate",
91
- "--disable-features=TranslateUI",
92
- "--disable-features=NetworkService",
93
- "--lang=en",
94
- "--disable-blink-features=AutomationControlled",
95
- ]
64
+ extra_args: list[str] = []
96
65
  async with async_playwright() as playwright:
97
66
  if cdp_address is not None:
98
67
  browser: Browser = await playwright.chromium.connect_over_cdp(cdp_address)
@@ -104,29 +73,25 @@ async def launch_chromium(
104
73
  user_preferences_dir,
105
74
  dir_to_clean,
106
75
  ) = await create_user_dir_with_preferences()
107
- if kwargs.get("proxy") is None:
108
- proxy_env = get_proxy_env()
109
- else:
110
- proxy_env = kwargs.get("proxy")
76
+ proxy = proxy or get_proxy_env()
111
77
  # Remove proxy from kwargs if it exists
112
- kwargs.pop("proxy", None)
113
- viewport = kwargs.get("viewport", {"width": 1280, "height": 800})
114
- kwargs.pop("viewport", None)
78
+ viewport = viewport or {"width": 1280, "height": 800}
115
79
 
116
- if headless:
117
- chromium_launch_args_to_ignore.append("--headless")
118
- extra_args.append("--headless=new")
80
+ if cdp_port:
81
+ extra_args.append(f"--remote-debugging-port={cdp_port}")
119
82
 
120
83
  context = await playwright.chromium.launch_persistent_context(
121
84
  os.fspath(user_preferences_dir),
122
85
  headless=headless,
123
86
  viewport=viewport,
124
- proxy=proxy_env,
125
- # ignore_default_args=chromium_launch_args_to_ignore,
87
+ proxy=proxy,
126
88
  user_agent=os.environ.get("USER_AGENT", default_user_agent),
127
- # args=extra_args,
89
+ args=extra_args,
128
90
  **kwargs,
129
91
  )
92
+ if cdp_port:
93
+ await wait_on_cdp_address(get_local_cdp_address(cdp_port))
94
+
130
95
  context.set_default_timeout(timeout * 1000)
131
96
 
132
97
  async def remove_dir_after_close(*_: Any, **__: Any) -> None:
@@ -145,6 +110,7 @@ async def launch_chromium(
145
110
  await process.wait()
146
111
 
147
112
  context.once("close", remove_dir_after_close)
113
+
148
114
  yield context, context.pages[0]
149
115
 
150
116
 
@@ -166,6 +132,8 @@ async def dangerous_launch_chromium(
166
132
  "--disable-features=NetworkService",
167
133
  "--lang=en",
168
134
  "--disable-blink-features=AutomationControlled",
135
+ "--disable-dev-shm-usage",
136
+ "--disk-cache-dir=/mnt/data/tmp/chrome-cache",
169
137
  ]
170
138
  playwright = await async_playwright().start()
171
139
  if cdp_url is not None:
@@ -173,6 +141,10 @@ async def dangerous_launch_chromium(
173
141
  browser: Browser = await playwright.chromium.connect_over_cdp(cdp_url)
174
142
  browser.on("disconnected", lambda _: logging.info("Browser Session disconnected"))
175
143
  context = browser.contexts[0]
144
+ # set view port for the already existing pages and any new pages
145
+ for page in context.pages:
146
+ await page.set_viewport_size(kwargs.get("viewport", {"width": 1280, "height": 800}))
147
+ context.on("page", lambda page: page.set_viewport_size(kwargs.get("viewport", {"width": 1280, "height": 800})))
176
148
  user_preferences_dir = None
177
149
  dir_to_clean = None
178
150
  else:
@@ -189,7 +161,6 @@ async def dangerous_launch_chromium(
189
161
  kwargs.pop("viewport", None)
190
162
 
191
163
  if headless:
192
- chromium_launch_args_to_ignore.append("--headless")
193
164
  extra_args.append("--headless=new")
194
165
 
195
166
  if port:
@@ -200,7 +171,6 @@ async def dangerous_launch_chromium(
200
171
  headless=headless,
201
172
  viewport=viewport,
202
173
  proxy=proxy_env,
203
- ignore_default_args=chromium_launch_args_to_ignore,
204
174
  user_agent=os.environ.get("USER_AGENT", default_user_agent),
205
175
  args=extra_args,
206
176
  **kwargs,
runtime/constants.py ADDED
@@ -0,0 +1 @@
1
+ api_key_header_name = "x-api-key"
@@ -21,6 +21,7 @@ class IntunedContext(BaseModel):
21
21
  extended_payloads: list[Payload] = Field(default_factory=lambda: list[Payload]())
22
22
  run_context: IntunedRunContext | None = None
23
23
  get_auth_session_parameters: Callable[[], Awaitable[dict[str, Any]]] | None = None
24
+ store: dict[str, Any] = Field(default_factory=lambda: dict())
24
25
 
25
26
  _token: Token["IntunedContext"] | None = None
26
27
 
runtime/env.py CHANGED
@@ -6,7 +6,7 @@ def get_workspace_id():
6
6
 
7
7
 
8
8
  def get_project_id():
9
- return os.environ.get("INTUNED_INTEGRATION_ID")
9
+ return os.environ.get("INTUNED_INTEGRATION_ID", os.environ.get("INTUNED_PROJECT_ID"))
10
10
 
11
11
 
12
12
  def get_functions_domain():
@@ -15,3 +15,17 @@ def get_functions_domain():
15
15
 
16
16
  def get_browser_type():
17
17
  return os.environ.get("BROWSER_TYPE")
18
+
19
+
20
+ def get_api_key():
21
+ return os.environ.get(api_key_env_var_key)
22
+
23
+
24
+ def get_is_running_in_cli():
25
+ return os.environ.get(cli_env_var_key) == "true"
26
+
27
+
28
+ cli_env_var_key = "INTUNED_CLI"
29
+ workspace_env_var_key = "INTUNED_WORKSPACE_ID"
30
+ project_env_var_key = "INTUNED_PROJECT_ID"
31
+ api_key_env_var_key = "INTUNED_API_KEY"
@@ -58,6 +58,14 @@ class NoAutomationInApiError(RunApiError):
58
58
  )
59
59
 
60
60
 
61
+ class InvalidAPIError(RunApiError):
62
+ def __init__(self, message: str):
63
+ super().__init__(
64
+ message,
65
+ "InvalidAPIError",
66
+ )
67
+
68
+
61
69
  class AutomationNotCoroutineError(RunApiError):
62
70
  def __init__(self, module: str, automation_function_name: str = "automation"):
63
71
  super().__init__(
@@ -1,5 +1,6 @@
1
+ from .attempt_store import attempt_store
1
2
  from .extend_payload import extend_payload
2
3
  from .extend_timeout import extend_timeout
3
4
  from .get_auth_session_parameters import get_auth_session_parameters
4
5
 
5
- __all__ = ["extend_payload", "extend_timeout", "get_auth_session_parameters"]
6
+ __all__ = ["extend_payload", "extend_timeout", "get_auth_session_parameters", "attempt_store"]
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+ from runtime.context.context import IntunedContext
4
+
5
+
6
+ class Store:
7
+ def get(self, key: str, default: Any = None) -> Any:
8
+ return IntunedContext.current().store.get(key, default)
9
+
10
+ def set(self, key: str, value: Any) -> None:
11
+ IntunedContext.current().store[key] = value
12
+
13
+
14
+ attempt_store = Store()
@@ -0,0 +1,147 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator
3
+ from contextlib import asynccontextmanager
4
+ from contextlib import AsyncExitStack
5
+ from typing import Any
6
+ from typing import AsyncContextManager
7
+ from typing import overload
8
+ from typing import TYPE_CHECKING
9
+ from typing import TypedDict
10
+ from typing import Unpack
11
+
12
+ from ..browser import launch_browser
13
+ from ..browser.helpers import get_local_cdp_address
14
+ from ..errors.run_api_errors import AutomationError
15
+ from ..errors.run_api_errors import InvalidAPIError
16
+ from ..run.types import ImportFunction
17
+ from .setup_context_hook import load_setup_context_hook
18
+ from .setup_context_hook import setup_context_hook_function_name
19
+
20
+ if TYPE_CHECKING:
21
+ from playwright.async_api import BrowserContext
22
+ from playwright.async_api import Page
23
+ from playwright.async_api import ProxySettings
24
+
25
+
26
+ class _CommonKwargs(TypedDict):
27
+ import_function: "ImportFunction"
28
+ api_name: str
29
+ api_parameters: Any
30
+
31
+
32
+ @overload
33
+ def playwright_context(
34
+ *,
35
+ proxy: "ProxySettings | None" = None,
36
+ headless: bool = False,
37
+ **kwargs: Unpack[_CommonKwargs],
38
+ ) -> "AsyncContextManager[tuple[BrowserContext, Page]]": ...
39
+ @overload
40
+ def playwright_context(
41
+ *,
42
+ cdp_address: str | None = None,
43
+ **kwargs: Unpack[_CommonKwargs],
44
+ ) -> "AsyncContextManager[tuple[BrowserContext, Page]]": ...
45
+
46
+
47
+ @asynccontextmanager
48
+ async def playwright_context(
49
+ *,
50
+ proxy: "ProxySettings | None" = None,
51
+ headless: bool = False,
52
+ cdp_address: str | None = None,
53
+ **kwargs: Unpack[_CommonKwargs],
54
+ ) -> "AsyncGenerator[tuple[BrowserContext, Page], None]":
55
+ setup_context_hook = load_setup_context_hook(import_function=kwargs["import_function"])
56
+
57
+ if setup_context_hook is None:
58
+ if cdp_address:
59
+ async with launch_browser(
60
+ cdp_address=cdp_address,
61
+ ) as (context, page):
62
+ yield context, page
63
+ else:
64
+ async with launch_browser(
65
+ proxy=proxy,
66
+ headless=headless,
67
+ ) as (context, page):
68
+ yield context, page
69
+ return
70
+
71
+ if cdp_address is not None:
72
+ cdp_port = None
73
+ hook_cdp_url = cdp_address
74
+ else:
75
+ cdp_port = await get_random_free_port()
76
+ hook_cdp_url = get_local_cdp_address(cdp_port)
77
+
78
+ async with AsyncExitStack() as stack:
79
+ if cdp_address:
80
+ context, page = await stack.enter_async_context(
81
+ launch_browser(
82
+ cdp_address=cdp_address,
83
+ )
84
+ )
85
+ else:
86
+ context, page = await stack.enter_async_context(
87
+ launch_browser(
88
+ proxy=proxy,
89
+ headless=headless,
90
+ cdp_port=cdp_port,
91
+ )
92
+ )
93
+ try:
94
+ hook_result = await setup_context_hook(
95
+ api_name=kwargs["api_name"],
96
+ api_parameters=kwargs["api_parameters"],
97
+ cdp_url=hook_cdp_url,
98
+ )
99
+ except Exception as e:
100
+ raise AutomationError(e) from e
101
+
102
+ if hook_result is None:
103
+ yield context, page
104
+ return
105
+
106
+ new_context: "BrowserContext | None" = None # noqa: UP037
107
+ try:
108
+ if not isinstance(hook_result, tuple):
109
+ new_context = hook_result
110
+ yield new_context, page
111
+ return
112
+
113
+ if len(hook_result) == 2:
114
+ new_context, new_page = hook_result
115
+
116
+ yield new_context, new_page or page
117
+ return
118
+
119
+ if len(hook_result) == 3:
120
+ new_context, new_page, cleanup = hook_result
121
+
122
+ try:
123
+ yield new_context, new_page or page
124
+ return
125
+ finally:
126
+ try:
127
+ await cleanup()
128
+ except Exception as e:
129
+ raise AutomationError(e) from e
130
+
131
+ raise InvalidAPIError(
132
+ f"{setup_context_hook_function_name} hook returned an invalid value. Return value must be one of: None, BrowserContext, (BrowserContext, Page | None), (BrowserContext, Page | None, Callable[..., Awaitable[None]])"
133
+ )
134
+ finally:
135
+ if new_context is not None and new_context != context:
136
+ await new_context.close()
137
+
138
+
139
+ async def get_random_free_port() -> int:
140
+ def get_random_free_port_sync():
141
+ import socket
142
+
143
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
144
+ s.bind(("", 0))
145
+ return s.getsockname()[1]
146
+
147
+ return await asyncio.to_thread(get_random_free_port_sync)
@@ -0,0 +1,27 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from playwright.async_api import BrowserContext
6
+
7
+ from anyio import Path
8
+
9
+
10
+ @asynccontextmanager
11
+ async def playwright_tracing(
12
+ *,
13
+ context: "BrowserContext",
14
+ trace_path: str,
15
+ screenshots: bool = True,
16
+ snapshots: bool = True,
17
+ sources: bool = True,
18
+ ):
19
+ await context.tracing.start(screenshots=screenshots, snapshots=snapshots, sources=sources)
20
+ try:
21
+ yield
22
+ finally:
23
+ try:
24
+ await context.tracing.stop(path=trace_path)
25
+ except Exception as e:
26
+ print("Error stopping tracing:", e)
27
+ await Path(trace_path).unlink(missing_ok=True)