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.
- 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.1.dist-info → intuned_runtime-1.2.3.dist-info}/METADATA +4 -2
- {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/RECORD +43 -36
- {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.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 +27 -57
- 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.1.dist-info → intuned_runtime-1.2.3.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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,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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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
|
117
|
-
|
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=
|
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
|
-
|
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"
|
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)
|