intuned-runtime 1.0.0__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 (58) hide show
  1. cli/__init__.py +45 -0
  2. cli/commands/__init__.py +25 -0
  3. cli/commands/ai_source/__init__.py +4 -0
  4. cli/commands/ai_source/ai_source.py +10 -0
  5. cli/commands/ai_source/deploy.py +64 -0
  6. cli/commands/browser/__init__.py +3 -0
  7. cli/commands/browser/save_state.py +32 -0
  8. cli/commands/init.py +127 -0
  9. cli/commands/project/__init__.py +20 -0
  10. cli/commands/project/auth_session/__init__.py +5 -0
  11. cli/commands/project/auth_session/check.py +118 -0
  12. cli/commands/project/auth_session/create.py +96 -0
  13. cli/commands/project/auth_session/load.py +39 -0
  14. cli/commands/project/project.py +10 -0
  15. cli/commands/project/run.py +340 -0
  16. cli/commands/project/run_interface.py +265 -0
  17. cli/commands/project/type_check.py +86 -0
  18. cli/commands/project/upgrade.py +92 -0
  19. cli/commands/publish_packages.py +264 -0
  20. cli/logger.py +19 -0
  21. cli/utils/ai_source_project.py +31 -0
  22. cli/utils/code_tree.py +83 -0
  23. cli/utils/run_apis.py +147 -0
  24. cli/utils/unix_socket.py +55 -0
  25. intuned_runtime-1.0.0.dist-info/LICENSE +42 -0
  26. intuned_runtime-1.0.0.dist-info/METADATA +113 -0
  27. intuned_runtime-1.0.0.dist-info/RECORD +58 -0
  28. intuned_runtime-1.0.0.dist-info/WHEEL +4 -0
  29. intuned_runtime-1.0.0.dist-info/entry_points.txt +3 -0
  30. runtime/__init__.py +3 -0
  31. runtime/backend_functions/__init__.py +5 -0
  32. runtime/backend_functions/_call_backend_function.py +86 -0
  33. runtime/backend_functions/get_auth_session_parameters.py +30 -0
  34. runtime/browser/__init__.py +3 -0
  35. runtime/browser/launch_chromium.py +212 -0
  36. runtime/browser/storage_state.py +106 -0
  37. runtime/context/__init__.py +5 -0
  38. runtime/context/context.py +51 -0
  39. runtime/env.py +13 -0
  40. runtime/errors/__init__.py +21 -0
  41. runtime/errors/auth_session_errors.py +9 -0
  42. runtime/errors/run_api_errors.py +120 -0
  43. runtime/errors/trace_errors.py +3 -0
  44. runtime/helpers/__init__.py +5 -0
  45. runtime/helpers/extend_payload.py +9 -0
  46. runtime/helpers/extend_timeout.py +13 -0
  47. runtime/helpers/get_auth_session_parameters.py +14 -0
  48. runtime/py.typed +0 -0
  49. runtime/run/__init__.py +3 -0
  50. runtime/run/intuned_settings.py +38 -0
  51. runtime/run/playwright_constructs.py +19 -0
  52. runtime/run/run_api.py +233 -0
  53. runtime/run/traces.py +36 -0
  54. runtime/types/__init__.py +15 -0
  55. runtime/types/payload.py +7 -0
  56. runtime/types/run_types.py +177 -0
  57. runtime_helpers/__init__.py +5 -0
  58. runtime_helpers/py.typed +0 -0
@@ -0,0 +1,106 @@
1
+ from typing import Any
2
+
3
+ from playwright.async_api import BrowserContext
4
+ from playwright.async_api import Error as PlaywrightError
5
+
6
+ from ..types.run_types import Cookie
7
+ from ..types.run_types import Origin
8
+ from ..types.run_types import SessionStorageOrigin
9
+ from ..types.run_types import StorageState
10
+
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def set_storage_state(context: BrowserContext, state: StorageState):
17
+ # Add cookies if they exist
18
+ await context.add_cookies(
19
+ [cookie.model_dump(by_alias=True) for cookie in state.cookies]
20
+ ) # type: ignore
21
+
22
+ # Apply localStorage for each origin
23
+ page = await context.new_page()
24
+ for origin_data in state.origins or []:
25
+ origin = origin_data.origin
26
+ await page.route(
27
+ f"{origin}/*",
28
+ lambda route: route.fulfill(
29
+ body="<html><head><title>Set Storage</title></head><body><h1>Set Storage</h1></body></html>",
30
+ content_type="text/html",
31
+ status=200,
32
+ ),
33
+ )
34
+ try:
35
+ await page.goto(origin)
36
+
37
+ # Set localStorage items
38
+ for item in origin_data.local_storage:
39
+ await page.evaluate(
40
+ """
41
+ ([key, value]) => {
42
+ window.localStorage.setItem(key, value)
43
+ }
44
+ """,
45
+ [item.name, item.value],
46
+ )
47
+ finally:
48
+ await page.unroute(origin)
49
+
50
+ # Apply sessionStorage if available
51
+ if state.session_storage is not None:
52
+ await context.add_init_script(f"""
53
+ const storage = {[s.model_dump(by_alias=True) for s in state.session_storage]};
54
+ for (const {{ origin, sessionStorage }} of storage) {{
55
+ if (window.location.origin === origin) {{
56
+ for (const item of sessionStorage){{
57
+ const value = window.sessionStorage.getItem(item.name);
58
+ console.log("value", value);
59
+ if (!value) {{
60
+ window.sessionStorage.setItem(item.name, item.value);
61
+ }}
62
+ }}
63
+ }}
64
+ }}
65
+ """)
66
+
67
+ await page.close()
68
+
69
+
70
+ async def get_storage_state(context: BrowserContext) -> StorageState:
71
+ storage_state = await context.storage_state()
72
+ cookies = storage_state.get("cookies") or []
73
+ origins = storage_state.get("origins") or []
74
+
75
+ session_storage: list[SessionStorageOrigin] = []
76
+ for page in context.pages:
77
+ try:
78
+ session_data: dict[str, Any] = await page.evaluate(
79
+ """
80
+ () => {
81
+ const items = { ...window.sessionStorage };
82
+ return {
83
+ origin: window.location.origin,
84
+ sessionStorage: Object.entries(items).map(([name, value]) => ({
85
+ name,
86
+ value,
87
+ })),
88
+ };
89
+ }
90
+ """
91
+ )
92
+ session_storage.append(SessionStorageOrigin(**session_data))
93
+ except PlaywrightError as e:
94
+ if "SecurityError" in e.message:
95
+ logger.warning(
96
+ f"Could not get storage state for page due '{page.url}' to security error."
97
+ )
98
+ continue
99
+ raise e
100
+
101
+ # Ignoring types here because it expects snake case from constructors, but we have alias configured to accept any case correctly
102
+ return StorageState(
103
+ cookies=[Cookie(**cookie) for cookie in cookies], # type: ignore
104
+ origins=[Origin(**origin) for origin in origins], # type: ignore
105
+ session_storage=session_storage,
106
+ )
@@ -0,0 +1,5 @@
1
+ from .context import IntunedContext
2
+
3
+ __all__ = [
4
+ "IntunedContext",
5
+ ]
@@ -0,0 +1,51 @@
1
+ from contextvars import ContextVar
2
+ from contextvars import Token
3
+ from typing import Any
4
+ from typing import Awaitable
5
+ from typing import Callable
6
+ from typing import ClassVar
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel
10
+ from pydantic import Field
11
+
12
+ from ..types import Payload
13
+ from ..types.run_types import IntunedRunContext
14
+
15
+
16
+ class IntunedContext(BaseModel):
17
+ _current: ClassVar[ContextVar["IntunedContext"]] = ContextVar("_current")
18
+
19
+ functions_token: str | None = None
20
+ extend_timeout: Callable[[], Awaitable[Any]] | None = None
21
+ extended_payloads: list[Payload] = Field(default_factory=lambda: list[Payload]())
22
+ run_context: IntunedRunContext | None = None
23
+ get_auth_session_parameters: Callable[[], Awaitable[dict[str, Any]]] | None = None
24
+
25
+ _token: Token["IntunedContext"] | None = None
26
+
27
+ @classmethod
28
+ def current(cls) -> "IntunedContext":
29
+ try:
30
+ current_context = cls._current.get()
31
+ except LookupError as e:
32
+ raise LookupError("No context found. Please use `IntunedContext.use()` to create a new context.") from e
33
+ return current_context
34
+
35
+ def __enter__(self) -> "IntunedContext":
36
+ """
37
+ Enter the context.
38
+ """
39
+ if self._token:
40
+ raise RuntimeError("Context was already entered with `__enter__`.")
41
+ self._token = self._current.set(self)
42
+ return self
43
+
44
+ def __exit__(self, exc_type: Optional[type], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
45
+ """
46
+ Exit the context.
47
+ """
48
+ if not self._token:
49
+ raise RuntimeError("Context was not entered with `__enter__`.")
50
+ self._current.reset(self._token)
51
+ self._token = None
runtime/env.py ADDED
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+
4
+ def get_workspace_id():
5
+ return os.environ.get("INTUNED_WORKSPACE_ID")
6
+
7
+
8
+ def get_project_id():
9
+ return os.environ.get("INTUNED_INTEGRATION_ID")
10
+
11
+
12
+ def get_functions_domain():
13
+ return os.environ.get("FUNCTIONS_DOMAIN")
@@ -0,0 +1,21 @@
1
+ from .run_api_errors import ApiNotFoundError
2
+ from .run_api_errors import AutomationError
3
+ from .run_api_errors import AutomationNotCoroutineError
4
+ from .run_api_errors import AutomationTimeoutError
5
+ from .run_api_errors import NoAutomationInApiError
6
+ from .run_api_errors import RunApiError
7
+ from .run_api_errors import RunIdNotProvidedError
8
+ from .run_api_errors import SessionMissingError
9
+ from .trace_errors import TraceNotFoundError
10
+
11
+ __all__ = [
12
+ "RunApiError",
13
+ "RunIdNotProvidedError",
14
+ "ApiNotFoundError",
15
+ "NoAutomationInApiError",
16
+ "AutomationNotCoroutineError",
17
+ "AutomationError",
18
+ "AutomationTimeoutError",
19
+ "TraceNotFoundError",
20
+ "SessionMissingError",
21
+ ]
@@ -0,0 +1,9 @@
1
+ from .run_api_errors import RunApiError
2
+
3
+
4
+ class AuthSessionsNotEnabledError(RunApiError):
5
+ def __init__(self):
6
+ super().__init__(
7
+ "Auth sessions are not enabled",
8
+ "AuthRequiredError",
9
+ )
@@ -0,0 +1,120 @@
1
+ from abc import ABC
2
+ from typing import Any
3
+ from typing import Iterable
4
+ from typing import Literal
5
+
6
+ RunErrorCode = Literal[
7
+ "APINotFoundError",
8
+ "InvalidAPIError",
9
+ "InvalidCheckError",
10
+ "AbortedError",
11
+ "AuthRequiredError",
12
+ "AuthCheckNotFoundError",
13
+ "AuthCheckFailedError",
14
+ "MaxLevelsExceededError",
15
+ "AutomationError",
16
+ "InternalInvalidInputError",
17
+ ]
18
+
19
+
20
+ class RunApiError(Exception, ABC):
21
+ def __init__(self, message: str, code: RunErrorCode):
22
+ super().__init__(message)
23
+ self.code = code
24
+ self.details: Any = None
25
+
26
+ @property
27
+ def json(self) -> dict[str, Any]:
28
+ return {
29
+ "code": self.code,
30
+ "details": self.details,
31
+ }
32
+
33
+
34
+ class RunIdNotProvidedError(RunApiError):
35
+ def __init__(self):
36
+ super().__init__(
37
+ "runId header not provided",
38
+ "InvalidAPIError",
39
+ )
40
+
41
+
42
+ class ApiNotFoundError(RunApiError):
43
+ def __init__(self, module: str):
44
+ super().__init__(
45
+ f"Module {module} not found",
46
+ "APINotFoundError",
47
+ )
48
+
49
+
50
+ class NoAutomationInApiError(RunApiError):
51
+ def __init__(self, module: str, automation_function_names: str | Iterable[str] = "automation"):
52
+ if isinstance(automation_function_names, str):
53
+ automation_function_names = [automation_function_names]
54
+ super().__init__(
55
+ f"Module {module} does not have {" or ".join([f"`{fn}`" for fn in automation_function_names])} function defined",
56
+ "APINotFoundError",
57
+ )
58
+
59
+
60
+ class AutomationNotCoroutineError(RunApiError):
61
+ def __init__(self, module: str, automation_function_name: str = "automation"):
62
+ super().__init__(
63
+ f"`{automation_function_name}` function in module {module} is not a coroutine function",
64
+ "InvalidAPIError",
65
+ )
66
+
67
+
68
+ class AutomationError(RunApiError):
69
+ def __init__(self, exception: BaseException):
70
+ # Get all public attributes of the exception
71
+ error_props = {key: str(value) for key, value in exception.__dict__.items() if not key.startswith("_")}
72
+
73
+ super().__init__(
74
+ str(exception),
75
+ "AutomationError",
76
+ )
77
+
78
+ self.details = {
79
+ "error_props": error_props,
80
+ "error_type": exception.__class__.__name__,
81
+ "name": exception.__class__.__name__,
82
+ "message": str(exception),
83
+ }
84
+
85
+
86
+ class AutomationTimeoutError(RunApiError):
87
+ def __init__(self):
88
+ super().__init__(
89
+ "Run timed out",
90
+ "AbortedError",
91
+ )
92
+
93
+
94
+ class SessionMissingError(RunApiError):
95
+ def __init__(self):
96
+ super().__init__(
97
+ "Session missing",
98
+ "AuthRequiredError",
99
+ )
100
+
101
+
102
+ class InvalidSessionError(RunApiError):
103
+ def __init__(self):
104
+ super().__init__(
105
+ "Invalid auth session",
106
+ "AuthCheckFailedError",
107
+ )
108
+
109
+
110
+ class InternalInvalidInputError(RunApiError):
111
+ def __init__(self, message: str, details: Any | None = None):
112
+ super().__init__(
113
+ f"Internal error: {message}. Please report this issue to the Intuned team.",
114
+ RunApiResponse(
115
+ status_code=500,
116
+ response={"error": "Internal error", "message": f"Internal error: {message}"},
117
+ ),
118
+ "InternalInvalidInputError",
119
+ )
120
+ self.details = details
@@ -0,0 +1,3 @@
1
+ class TraceNotFoundError(Exception):
2
+ def __init__(self):
3
+ super().__init__("Trace file not found")
@@ -0,0 +1,5 @@
1
+ from .extend_payload import extend_payload
2
+ from .extend_timeout import extend_timeout
3
+ from .get_auth_session_parameters import get_auth_session_parameters
4
+
5
+ __all__ = ["extend_payload", "extend_timeout", "get_auth_session_parameters"]
@@ -0,0 +1,9 @@
1
+ from runtime.context.context import IntunedContext
2
+
3
+ from ..types import Payload
4
+ from .extend_timeout import extend_timeout
5
+
6
+
7
+ def extend_payload(*payload: Payload):
8
+ IntunedContext.current().extended_payloads += [*payload]
9
+ extend_timeout()
@@ -0,0 +1,13 @@
1
+ import asyncio
2
+
3
+ from runtime.context.context import IntunedContext
4
+
5
+ _debounce_time = 60 # seconds
6
+
7
+
8
+ def extend_timeout():
9
+ context = IntunedContext.current()
10
+
11
+ if context.extend_timeout is not None:
12
+ call_extend_timeout_api = context.extend_timeout
13
+ asyncio.create_task(asyncio.wait_for(call_extend_timeout_api(), timeout=10))
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+ from runtime.context.context import IntunedContext
4
+
5
+
6
+ async def get_auth_session_parameters() -> dict[str, Any]:
7
+ """
8
+ Get the auth session parameters from the IntunedContext.
9
+ """
10
+ context = IntunedContext.current()
11
+ if context.get_auth_session_parameters is None:
12
+ raise Exception("get_auth_session_parameters failed due to an internal error (context was not found).")
13
+
14
+ return await context.get_auth_session_parameters()
runtime/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from .run_api import run_api
2
+
3
+ __all__ = ["run_api"]
@@ -0,0 +1,38 @@
1
+ import json
2
+ import os
3
+ from dataclasses import dataclass
4
+ from dataclasses import field
5
+ from typing import Any
6
+
7
+ from aiofiles import open
8
+
9
+
10
+ @dataclass
11
+ class AuthSessions:
12
+ enabled: bool = False
13
+
14
+
15
+ @dataclass
16
+ class IntunedSettings:
17
+ auth_sessions: AuthSessions = field(default_factory=AuthSessions)
18
+
19
+
20
+ default_settings = IntunedSettings()
21
+
22
+
23
+ def validate_settings(settings_dict: dict[Any, Any]) -> IntunedSettings:
24
+ auth_sessions = AuthSessions(enabled=settings_dict.get("authSessions", {}).get("enabled", False))
25
+ return IntunedSettings(auth_sessions=auth_sessions)
26
+
27
+
28
+ async def load_intuned_settings() -> IntunedSettings:
29
+ settings_path = os.path.join(os.getcwd(), "Intuned.json")
30
+ if not os.path.exists(settings_path):
31
+ return default_settings
32
+ try:
33
+ async with open(settings_path) as settings_file:
34
+ content = await settings_file.read()
35
+ settings_dict = json.loads(content)
36
+ return validate_settings(settings_dict)
37
+ except json.JSONDecodeError as e:
38
+ raise Exception("Invalid Intuned.json file") from e
@@ -0,0 +1,19 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from playwright.async_api import ProxySettings
4
+
5
+ from ..browser import launch_chromium
6
+
7
+
8
+ @asynccontextmanager
9
+ async def get_production_playwright_constructs(
10
+ proxy: ProxySettings | None = None,
11
+ headless: bool = False,
12
+ *,
13
+ cdp_address: str | None = None,
14
+ ):
15
+ async with launch_chromium(headless=headless, cdp_address=cdp_address, proxy=proxy) as (context, page):
16
+ try:
17
+ yield context, page
18
+ finally:
19
+ await context.close()
runtime/run/run_api.py ADDED
@@ -0,0 +1,233 @@
1
+ import asyncio
2
+ import functools
3
+ import json
4
+ import os.path
5
+ from importlib import import_module
6
+ from importlib.util import module_from_spec
7
+ from importlib.util import spec_from_file_location
8
+ from inspect import iscoroutinefunction
9
+ from typing import Any
10
+ from typing import Callable
11
+ from typing import Coroutine
12
+ from typing import Optional
13
+ from typing import Protocol
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
19
+
20
+ from runtime.browser.storage_state import get_storage_state
21
+ from runtime.browser.storage_state import set_storage_state
22
+ from runtime.context import IntunedContext
23
+ from runtime.errors.run_api_errors import InvalidSessionError
24
+ from runtime.errors.run_api_errors import RunApiError
25
+ from runtime.types import RunAutomationSuccessResult
26
+ from runtime.types.run_types import PayloadToAppend
27
+ from runtime.types.run_types import RunApiParameters
28
+
29
+ from ..errors import ApiNotFoundError
30
+ from ..errors import AutomationError
31
+ from ..errors import AutomationNotCoroutineError
32
+ from ..errors import NoAutomationInApiError
33
+ from .playwright_constructs import get_production_playwright_constructs
34
+ from .playwright_constructs import ProxySettings
35
+
36
+
37
+ def import_function_from_api_dir(
38
+ *,
39
+ file_path: str,
40
+ base_dir: Optional[str] = None,
41
+ automation_function_name: str | None = None,
42
+ ) -> Callable[..., Coroutine[Any, Any, Any]]:
43
+ module_path = file_path.replace("/", ".")
44
+
45
+ def _import_module():
46
+ if base_dir is None:
47
+ return import_module(module_path)
48
+ else:
49
+ file_location = os.path.join(base_dir, f"{file_path}.py")
50
+ if not os.path.exists(file_location):
51
+ raise ApiNotFoundError(module_path)
52
+ spec = spec_from_file_location(os.path.basename(file_path), file_location)
53
+ if spec is None:
54
+ raise ApiNotFoundError(module_path)
55
+ module = module_from_spec(spec)
56
+ if spec.loader is None:
57
+ raise ApiNotFoundError(module_path)
58
+ spec.loader.exec_module(module)
59
+ return module
60
+
61
+ try:
62
+ module = _import_module()
63
+
64
+ except ModuleNotFoundError as e:
65
+ # if the top-level module does not exist, it is a 404
66
+ if e.name == module_path:
67
+ raise ApiNotFoundError(module_path) from e
68
+
69
+ # otherwise, it is an import error inside the user code
70
+ raise AutomationError(e) from e
71
+ except RunApiError:
72
+ raise
73
+ except BaseException as e:
74
+ raise AutomationError(e) from e
75
+
76
+ automation_functions_to_try: list[str] = []
77
+ if automation_function_name is not None:
78
+ automation_functions_to_try.append(automation_function_name)
79
+ else:
80
+ automation_functions_to_try.append("automation")
81
+ automation_functions_to_try.append("create")
82
+ automation_functions_to_try.append("check")
83
+
84
+ err: AttributeError | None = None
85
+ automation_coroutine = None
86
+
87
+ name = automation_functions_to_try[0]
88
+ for n in automation_functions_to_try:
89
+ name = n
90
+ try:
91
+ automation_coroutine = getattr(module, name)
92
+ except AttributeError as e:
93
+ err = e
94
+ else:
95
+ break
96
+
97
+ if automation_coroutine is None:
98
+ raise NoAutomationInApiError(module_path, automation_functions_to_try) from err
99
+
100
+ if not iscoroutinefunction(automation_coroutine):
101
+ raise AutomationNotCoroutineError(module_path)
102
+
103
+ return automation_coroutine
104
+
105
+
106
+ class ImportFunction(Protocol):
107
+ def __call__(self, file_path: str, name: Optional[str] = None, /) -> Callable[..., Coroutine[Any, Any, Any]]: ...
108
+
109
+
110
+ async def run_api(
111
+ parameters: RunApiParameters,
112
+ *,
113
+ import_function: ImportFunction | None = None,
114
+ ):
115
+ trace_started: bool = False
116
+
117
+ headless = False
118
+ proxy = None
119
+ cdp_address = None
120
+
121
+ if parameters.run_options.environment == "standalone":
122
+ headless = parameters.run_options.headless
123
+ proxy_config = parameters.run_options.proxy
124
+ if proxy_config is not None:
125
+ proxy = ProxySettings(
126
+ **proxy_config.model_dump(by_alias=True),
127
+ )
128
+ else:
129
+ cdp_address = parameters.run_options.cdp_address
130
+
131
+ async with get_production_playwright_constructs(
132
+ headless=headless,
133
+ proxy=proxy,
134
+ cdp_address=cdp_address,
135
+ ) as (context, page):
136
+ if parameters.tracing.enabled:
137
+ await context.tracing.start(screenshots=True, snapshots=True, sources=True)
138
+ trace_started = True
139
+
140
+ if parameters.auth is not None and parameters.auth.session.type == "state":
141
+ if parameters.auth.session.state is None:
142
+ raise InvalidSessionError()
143
+ state = parameters.auth.session.state
144
+ await set_storage_state(
145
+ context=context,
146
+ state=state,
147
+ )
148
+
149
+ import_function = import_function or (
150
+ lambda file_path, name=None: import_function_from_api_dir(
151
+ file_path=file_path, automation_function_name=name
152
+ )
153
+ )
154
+
155
+ async def _run_automation():
156
+ try:
157
+ automation_coroutine = import_function(parameters.automation_function.name)
158
+
159
+ if parameters.auth is not None and parameters.auth.run_check:
160
+ retry_configs = retry(
161
+ stop=stop_after_attempt(2),
162
+ retry=retry_if_not_result(lambda result: result is True),
163
+ reraise=True,
164
+ )
165
+
166
+ check_fn = import_function("auth-sessions/check")
167
+
168
+ check_fn_with_retries = retry_configs(check_fn)
169
+ try:
170
+ check_result = await check_fn_with_retries(page)
171
+ except RetryError:
172
+ check_result = False
173
+ if type(check_result) is not bool:
174
+ raise AutomationError(TypeError("Check function must return a boolean"))
175
+ if not check_result:
176
+ raise InvalidSessionError()
177
+
178
+ automation_coroutine_with_page = functools.partial(automation_coroutine, page)
179
+ if parameters.automation_function.params is None:
180
+ automation_result = await automation_coroutine_with_page()
181
+ else:
182
+ automation_result = await automation_coroutine_with_page(parameters.automation_function.params)
183
+ try:
184
+ json.dumps(automation_result)
185
+ except TypeError as e:
186
+ raise AutomationError(TypeError("Result is not JSON serializable")) from e
187
+
188
+ response = RunAutomationSuccessResult(
189
+ result=automation_result,
190
+ )
191
+ extended_payloads = IntunedContext.current().extended_payloads
192
+ if extended_payloads:
193
+ response.payload_to_append = [
194
+ PayloadToAppend(
195
+ api_name=payload["api"],
196
+ parameters=payload["parameters"],
197
+ )
198
+ for payload in extended_payloads
199
+ ]
200
+ if parameters.retrieve_session:
201
+ response.session = await get_storage_state(context)
202
+ return response
203
+ except RunApiError as e:
204
+ raise e
205
+ except Exception as e:
206
+ # Get all public attributes of the exception
207
+ raise AutomationError(e) from e
208
+
209
+ automation_task = None
210
+ try:
211
+ automation_task = asyncio.create_task(_run_automation())
212
+
213
+ # Shield will make the CancelledError get thrown directly here instead of inside `automation_task`
214
+ result = await asyncio.shield(automation_task)
215
+ return result
216
+ except asyncio.CancelledError:
217
+ # Manually cancel the automation task
218
+ if automation_task and not automation_task.done():
219
+ automation_task.cancel()
220
+ try:
221
+ # Wait for the automation task to be cancelled for a brief moment
222
+ await asyncio.wait_for(automation_task, timeout=0.1)
223
+ except (asyncio.CancelledError, asyncio.TimeoutError):
224
+ pass
225
+ raise # Re-raise the cancellation
226
+ finally:
227
+ if parameters.tracing.enabled is True and trace_started:
228
+ try:
229
+ await context.tracing.stop(path=parameters.tracing.file_path)
230
+ except Exception as e:
231
+ print("Error stopping tracing:", e)
232
+ os.remove(parameters.tracing.file_path)
233
+ await context.close()