intuned-runtime 1.2.0__py3-none-any.whl → 1.2.2__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.
- intuned_cli/__init__.py +12 -2
- intuned_cli/commands/__init__.py +1 -0
- intuned_cli/commands/attempt_api_command.py +1 -1
- intuned_cli/commands/attempt_authsession_check_command.py +1 -1
- intuned_cli/commands/attempt_authsession_create_command.py +1 -1
- intuned_cli/commands/deploy_command.py +4 -4
- intuned_cli/commands/run_api_command.py +1 -1
- intuned_cli/commands/run_authsession_create_command.py +1 -1
- intuned_cli/commands/run_authsession_update_command.py +1 -1
- intuned_cli/commands/run_authsession_validate_command.py +1 -1
- intuned_cli/commands/save_command.py +47 -0
- intuned_cli/controller/__test__/test_api.py +0 -1
- intuned_cli/controller/api.py +0 -1
- intuned_cli/controller/authsession.py +0 -1
- intuned_cli/controller/deploy.py +8 -210
- intuned_cli/controller/save.py +260 -0
- intuned_cli/utils/backend.py +26 -0
- intuned_cli/utils/error.py +7 -3
- intuned_internal_cli/commands/project/auth_session/check.py +0 -1
- intuned_internal_cli/commands/project/run.py +0 -2
- intuned_runtime/__init__.py +2 -1
- {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.dist-info}/METADATA +4 -2
- {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.dist-info}/RECORD +43 -36
- {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.dist-info}/WHEEL +1 -1
- runtime/backend_functions/_call_backend_function.py +18 -2
- runtime/browser/helpers.py +22 -0
- runtime/browser/launch_browser.py +43 -15
- runtime/browser/launch_chromium.py +26 -22
- runtime/constants.py +1 -0
- runtime/context/context.py +1 -0
- runtime/env.py +15 -1
- runtime/errors/run_api_errors.py +8 -0
- runtime/helpers/__init__.py +2 -1
- runtime/helpers/attempt_store.py +14 -0
- runtime/run/playwright_context.py +147 -0
- runtime/run/playwright_tracing.py +27 -0
- runtime/run/run_api.py +71 -91
- runtime/run/setup_context_hook.py +40 -0
- runtime/run/types.py +14 -0
- runtime/types/run_types.py +0 -1
- runtime_helpers/__init__.py +2 -1
- runtime/run/playwright_constructs.py +0 -20
- {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.dist-info}/entry_points.txt +0 -0
- {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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,13 +4,20 @@ 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
|
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from playwright.async_api import ProxySettings
|
19
|
+
from playwright.async_api import ViewportSize
|
20
|
+
|
14
21
|
|
15
22
|
chromium_launch_args_to_ignore = [
|
16
23
|
"--disable-field-trial-config",
|
@@ -78,21 +85,17 @@ default_user_agent = (
|
|
78
85
|
async def launch_chromium(
|
79
86
|
headless: bool = True,
|
80
87
|
timeout: int = 10,
|
88
|
+
cdp_port: int | None = None,
|
81
89
|
cdp_address: str | None = None,
|
90
|
+
*,
|
91
|
+
proxy: "ProxySettings | None" = None,
|
92
|
+
viewport: "ViewportSize | None" = None,
|
82
93
|
**kwargs: Any,
|
83
94
|
):
|
84
95
|
from playwright.async_api import async_playwright
|
85
96
|
from playwright.async_api import Browser
|
86
97
|
|
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
|
-
]
|
98
|
+
extra_args: list[str] = []
|
96
99
|
async with async_playwright() as playwright:
|
97
100
|
if cdp_address is not None:
|
98
101
|
browser: Browser = await playwright.chromium.connect_over_cdp(cdp_address)
|
@@ -104,29 +107,25 @@ async def launch_chromium(
|
|
104
107
|
user_preferences_dir,
|
105
108
|
dir_to_clean,
|
106
109
|
) = await create_user_dir_with_preferences()
|
107
|
-
|
108
|
-
proxy_env = get_proxy_env()
|
109
|
-
else:
|
110
|
-
proxy_env = kwargs.get("proxy")
|
110
|
+
proxy = proxy or get_proxy_env()
|
111
111
|
# Remove proxy from kwargs if it exists
|
112
|
-
|
113
|
-
viewport = kwargs.get("viewport", {"width": 1280, "height": 800})
|
114
|
-
kwargs.pop("viewport", None)
|
112
|
+
viewport = viewport or {"width": 1280, "height": 800}
|
115
113
|
|
116
|
-
if
|
117
|
-
|
118
|
-
extra_args.append("--headless=new")
|
114
|
+
if cdp_port:
|
115
|
+
extra_args.append(f"--remote-debugging-port={cdp_port}")
|
119
116
|
|
120
117
|
context = await playwright.chromium.launch_persistent_context(
|
121
118
|
os.fspath(user_preferences_dir),
|
122
119
|
headless=headless,
|
123
120
|
viewport=viewport,
|
124
|
-
proxy=
|
125
|
-
# ignore_default_args=chromium_launch_args_to_ignore,
|
121
|
+
proxy=proxy,
|
126
122
|
user_agent=os.environ.get("USER_AGENT", default_user_agent),
|
127
|
-
|
123
|
+
args=extra_args,
|
128
124
|
**kwargs,
|
129
125
|
)
|
126
|
+
if cdp_port:
|
127
|
+
await wait_on_cdp_address(get_local_cdp_address(cdp_port))
|
128
|
+
|
130
129
|
context.set_default_timeout(timeout * 1000)
|
131
130
|
|
132
131
|
async def remove_dir_after_close(*_: Any, **__: Any) -> None:
|
@@ -145,6 +144,7 @@ async def launch_chromium(
|
|
145
144
|
await process.wait()
|
146
145
|
|
147
146
|
context.once("close", remove_dir_after_close)
|
147
|
+
|
148
148
|
yield context, context.pages[0]
|
149
149
|
|
150
150
|
|
@@ -173,6 +173,10 @@ async def dangerous_launch_chromium(
|
|
173
173
|
browser: Browser = await playwright.chromium.connect_over_cdp(cdp_url)
|
174
174
|
browser.on("disconnected", lambda _: logging.info("Browser Session disconnected"))
|
175
175
|
context = browser.contexts[0]
|
176
|
+
# set view port for the already existing pages and any new pages
|
177
|
+
for page in context.pages:
|
178
|
+
await page.set_viewport_size(kwargs.get("viewport", {"width": 1280, "height": 800}))
|
179
|
+
context.on("page", lambda page: page.set_viewport_size(kwargs.get("viewport", {"width": 1280, "height": 800})))
|
176
180
|
user_preferences_dir = None
|
177
181
|
dir_to_clean = None
|
178
182
|
else:
|
runtime/constants.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
api_key_header_name = "x-api-key"
|
runtime/context/context.py
CHANGED
@@ -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"
|
runtime/errors/run_api_errors.py
CHANGED
@@ -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__(
|
runtime/helpers/__init__.py
CHANGED
@@ -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)
|
runtime/run/run_api.py
CHANGED
@@ -1,21 +1,18 @@
|
|
1
1
|
import asyncio
|
2
2
|
import functools
|
3
3
|
import json
|
4
|
-
import
|
4
|
+
import sys
|
5
|
+
from collections.abc import Callable
|
6
|
+
from contextlib import AsyncExitStack
|
5
7
|
from importlib import import_module
|
6
|
-
from importlib.util import module_from_spec
|
7
|
-
from importlib.util import spec_from_file_location
|
8
8
|
from inspect import iscoroutinefunction
|
9
9
|
from typing import Any
|
10
|
-
from typing import
|
11
|
-
from typing import
|
10
|
+
from typing import Awaitable
|
11
|
+
from typing import cast
|
12
12
|
from typing import Optional
|
13
13
|
from typing import Protocol
|
14
14
|
|
15
|
-
from
|
16
|
-
from tenacity import retry_if_not_result
|
17
|
-
from tenacity import RetryError
|
18
|
-
from tenacity import stop_after_attempt
|
15
|
+
from git import TYPE_CHECKING
|
19
16
|
|
20
17
|
from runtime.browser.storage_state import get_storage_state
|
21
18
|
from runtime.browser.storage_state import set_storage_state
|
@@ -23,6 +20,7 @@ from runtime.context import IntunedContext
|
|
23
20
|
from runtime.errors.run_api_errors import InvalidSessionError
|
24
21
|
from runtime.errors.run_api_errors import ResultTooBigError
|
25
22
|
from runtime.errors.run_api_errors import RunApiError
|
23
|
+
from runtime.run.playwright_context import playwright_context
|
26
24
|
from runtime.types import RunAutomationSuccessResult
|
27
25
|
from runtime.types.run_types import PayloadToAppend
|
28
26
|
from runtime.types.run_types import RunApiParameters
|
@@ -31,8 +29,12 @@ from ..errors import ApiNotFoundError
|
|
31
29
|
from ..errors import AutomationError
|
32
30
|
from ..errors import AutomationNotCoroutineError
|
33
31
|
from ..errors import NoAutomationInApiError
|
34
|
-
from .
|
32
|
+
from .playwright_tracing import playwright_tracing
|
35
33
|
from .pydantic_encoder import PydanticEncoder
|
34
|
+
from .types import ImportFunction
|
35
|
+
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
from playwright.async_api import Page
|
36
38
|
|
37
39
|
|
38
40
|
def get_object_size_in_bytes(obj: Any) -> int:
|
@@ -49,31 +51,20 @@ def import_function_from_api_dir(
|
|
49
51
|
file_path: str,
|
50
52
|
base_dir: Optional[str] = None,
|
51
53
|
automation_function_name: str | None = None,
|
52
|
-
) -> Callable[...,
|
54
|
+
) -> Callable[..., Awaitable[Any]]:
|
53
55
|
module_path = file_path.replace("/", ".")
|
54
56
|
|
55
57
|
def _import_module():
|
56
|
-
if base_dir is None:
|
57
|
-
|
58
|
-
|
59
|
-
file_location = os.path.join(base_dir, f"{file_path}.py")
|
60
|
-
if not os.path.exists(file_location):
|
61
|
-
raise ApiNotFoundError(module_path)
|
62
|
-
spec = spec_from_file_location(os.path.basename(file_path), file_location)
|
63
|
-
if spec is None:
|
64
|
-
raise ApiNotFoundError(module_path)
|
65
|
-
module = module_from_spec(spec)
|
66
|
-
if spec.loader is None:
|
67
|
-
raise ApiNotFoundError(module_path)
|
68
|
-
spec.loader.exec_module(module)
|
69
|
-
return module
|
58
|
+
if base_dir is not None:
|
59
|
+
sys.path.insert(0, base_dir)
|
60
|
+
return import_module(module_path)
|
70
61
|
|
71
62
|
try:
|
72
63
|
module = _import_module()
|
73
64
|
|
74
65
|
except ModuleNotFoundError as e:
|
75
66
|
# if the top-level module does not exist, it is a 404
|
76
|
-
if e.name == module_path:
|
67
|
+
if e.name == module_path or e.name == module_path.split(".", 1)[0]:
|
77
68
|
raise ApiNotFoundError(module_path) from e
|
78
69
|
|
79
70
|
# otherwise, it is an import error inside the user code
|
@@ -113,8 +104,10 @@ def import_function_from_api_dir(
|
|
113
104
|
return automation_coroutine
|
114
105
|
|
115
106
|
|
116
|
-
class
|
117
|
-
def __call__(
|
107
|
+
class AutomationFunction(Protocol):
|
108
|
+
def __call__(
|
109
|
+
self, page: "Page", parameters: dict[Any, Any] | None = None, *args: Any, **kwargs: Any
|
110
|
+
) -> Awaitable[Any]: ...
|
118
111
|
|
119
112
|
|
120
113
|
async def run_api(
|
@@ -124,30 +117,50 @@ async def run_api(
|
|
124
117
|
) -> RunAutomationSuccessResult:
|
125
118
|
from playwright.async_api import ProxySettings
|
126
119
|
|
127
|
-
|
120
|
+
import_function = import_function or (
|
121
|
+
lambda file_path, name=None: import_function_from_api_dir(file_path=file_path, automation_function_name=name)
|
122
|
+
)
|
128
123
|
|
129
|
-
|
130
|
-
|
131
|
-
|
124
|
+
async with AsyncExitStack() as stack:
|
125
|
+
_initialize_playwright_context = functools.partial(
|
126
|
+
playwright_context,
|
127
|
+
import_function=import_function,
|
128
|
+
api_name=parameters.automation_function.name,
|
129
|
+
api_parameters=parameters.automation_function.params,
|
130
|
+
)
|
131
|
+
if parameters.run_options.environment == "standalone":
|
132
|
+
proxy_config = parameters.run_options.proxy
|
133
|
+
if proxy_config is not None:
|
134
|
+
proxy = ProxySettings(
|
135
|
+
**proxy_config.model_dump(by_alias=True),
|
136
|
+
)
|
137
|
+
else:
|
138
|
+
proxy = None
|
132
139
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
140
|
+
context, page = await stack.enter_async_context(
|
141
|
+
_initialize_playwright_context(
|
142
|
+
proxy=proxy,
|
143
|
+
headless=parameters.run_options.headless,
|
144
|
+
)
|
145
|
+
)
|
146
|
+
|
147
|
+
else:
|
148
|
+
context, page = await stack.enter_async_context(
|
149
|
+
_initialize_playwright_context(
|
150
|
+
cdp_address=parameters.run_options.cdp_address,
|
151
|
+
)
|
139
152
|
)
|
140
|
-
else:
|
141
|
-
cdp_address = parameters.run_options.cdp_address
|
142
153
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
154
|
+
if parameters.tracing.enabled is True:
|
155
|
+
await stack.enter_async_context(
|
156
|
+
playwright_tracing(
|
157
|
+
context=context,
|
158
|
+
trace_path=parameters.tracing.file_path,
|
159
|
+
screenshots=True,
|
160
|
+
snapshots=True,
|
161
|
+
sources=True,
|
162
|
+
)
|
163
|
+
)
|
151
164
|
|
152
165
|
if parameters.auth is not None and parameters.auth.session.type == "state":
|
153
166
|
if parameters.auth.session.state is None:
|
@@ -157,40 +170,16 @@ async def run_api(
|
|
157
170
|
context=context,
|
158
171
|
state=state,
|
159
172
|
)
|
160
|
-
import_function = import_function or (
|
161
|
-
lambda file_path, name=None: import_function_from_api_dir(
|
162
|
-
file_path=file_path, automation_function_name=name
|
163
|
-
)
|
164
|
-
)
|
165
173
|
|
166
174
|
async def _run_automation():
|
167
175
|
try:
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
retry_configs = retry(
|
172
|
-
stop=stop_after_attempt(2),
|
173
|
-
retry=retry_if_not_result(lambda result: result is True),
|
174
|
-
reraise=True,
|
175
|
-
)
|
176
|
-
|
177
|
-
check_fn = import_function("auth-sessions/check")
|
178
|
-
|
179
|
-
check_fn_with_retries = retry_configs(check_fn)
|
180
|
-
try:
|
181
|
-
check_result = await check_fn_with_retries(page)
|
182
|
-
except RetryError:
|
183
|
-
check_result = False
|
184
|
-
if type(check_result) is not bool:
|
185
|
-
raise AutomationError(TypeError("Check function must return a boolean"))
|
186
|
-
if not check_result:
|
187
|
-
raise InvalidSessionError()
|
188
|
-
|
189
|
-
automation_coroutine_with_page = functools.partial(automation_coroutine, page)
|
176
|
+
automation_function = cast(AutomationFunction, import_function(parameters.automation_function.name))
|
177
|
+
|
178
|
+
automation_function = functools.partial(automation_function, page)
|
190
179
|
if parameters.automation_function.params is None:
|
191
|
-
automation_result = await
|
180
|
+
automation_result = await automation_function()
|
192
181
|
else:
|
193
|
-
automation_result = await
|
182
|
+
automation_result = await automation_function(parameters.automation_function.params)
|
194
183
|
try:
|
195
184
|
automation_result = json.loads(json.dumps(automation_result, cls=PydanticEncoder))
|
196
185
|
except TypeError as e:
|
@@ -228,16 +217,13 @@ async def run_api(
|
|
228
217
|
# Get all public attributes of the exception
|
229
218
|
raise AutomationError(e) from e
|
230
219
|
|
231
|
-
automation_task =
|
220
|
+
automation_task = asyncio.create_task(_run_automation())
|
232
221
|
try:
|
233
|
-
automation_task = asyncio.create_task(_run_automation())
|
234
|
-
|
235
222
|
# Shield will make the CancelledError get thrown directly here instead of inside `automation_task`
|
236
|
-
|
237
|
-
return result
|
223
|
+
return await asyncio.shield(automation_task)
|
238
224
|
except asyncio.CancelledError:
|
239
225
|
# Manually cancel the automation task
|
240
|
-
if
|
226
|
+
if not automation_task.done():
|
241
227
|
automation_task.cancel()
|
242
228
|
try:
|
243
229
|
# Wait for the automation task to be cancelled for a brief moment
|
@@ -245,11 +231,5 @@ async def run_api(
|
|
245
231
|
except (asyncio.CancelledError, asyncio.TimeoutError):
|
246
232
|
pass
|
247
233
|
raise # Re-raise the cancellation
|
248
|
-
|
249
|
-
|
250
|
-
try:
|
251
|
-
await context.tracing.stop(path=parameters.tracing.file_path)
|
252
|
-
except Exception as e:
|
253
|
-
print("Error stopping tracing:", e)
|
254
|
-
os.remove(parameters.tracing.file_path)
|
255
|
-
await context.close()
|
234
|
+
|
235
|
+
raise RuntimeError("Unreachable code path")
|