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.
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.0.dist-info → intuned_runtime-1.2.2.dist-info}/METADATA +4 -2
  23. {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.dist-info}/RECORD +43 -36
  24. {intuned_runtime-1.2.0.dist-info → intuned_runtime-1.2.2.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 +26 -22
  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.0.dist-info → intuned_runtime-1.2.2.dist-info}/entry_points.txt +0 -0
  44. {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
- 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,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
- if kwargs.get("proxy") is None:
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
- kwargs.pop("proxy", None)
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 headless:
117
- chromium_launch_args_to_ignore.append("--headless")
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=proxy_env,
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
- # args=extra_args,
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"
@@ -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)
runtime/run/run_api.py CHANGED
@@ -1,21 +1,18 @@
1
1
  import asyncio
2
2
  import functools
3
3
  import json
4
- import os.path
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 Callable
11
- from typing import Coroutine
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 tenacity import retry
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 .playwright_constructs import get_production_playwright_constructs
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[..., Coroutine[Any, Any, Any]]:
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
- return import_module(module_path)
58
- else:
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 ImportFunction(Protocol):
117
- def __call__(self, file_path: str, name: Optional[str] = None, /) -> Callable[..., Coroutine[Any, Any, Any]]: ...
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
- trace_started: bool = False
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
- headless = False
130
- proxy = None
131
- cdp_address = None
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
- if parameters.run_options.environment == "standalone":
134
- headless = parameters.run_options.headless
135
- proxy_config = parameters.run_options.proxy
136
- if proxy_config is not None:
137
- proxy = ProxySettings(
138
- **proxy_config.model_dump(by_alias=True),
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
- async with get_production_playwright_constructs(
144
- headless=headless,
145
- proxy=proxy,
146
- cdp_address=cdp_address,
147
- ) as (context, page):
148
- if parameters.tracing.enabled:
149
- await context.tracing.start(screenshots=True, snapshots=True, sources=True)
150
- trace_started = True
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
- automation_coroutine = import_function(parameters.automation_function.name)
169
-
170
- if parameters.auth is not None and parameters.auth.run_check:
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 automation_coroutine_with_page()
180
+ automation_result = await automation_function()
192
181
  else:
193
- automation_result = await automation_coroutine_with_page(parameters.automation_function.params)
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 = None
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
- result = await asyncio.shield(automation_task)
237
- return result
223
+ return await asyncio.shield(automation_task)
238
224
  except asyncio.CancelledError:
239
225
  # Manually cancel the automation task
240
- if automation_task and not automation_task.done():
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
- finally:
249
- if parameters.tracing.enabled is True and trace_started:
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")