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.
- cli/__init__.py +45 -0
- cli/commands/__init__.py +25 -0
- cli/commands/ai_source/__init__.py +4 -0
- cli/commands/ai_source/ai_source.py +10 -0
- cli/commands/ai_source/deploy.py +64 -0
- cli/commands/browser/__init__.py +3 -0
- cli/commands/browser/save_state.py +32 -0
- cli/commands/init.py +127 -0
- cli/commands/project/__init__.py +20 -0
- cli/commands/project/auth_session/__init__.py +5 -0
- cli/commands/project/auth_session/check.py +118 -0
- cli/commands/project/auth_session/create.py +96 -0
- cli/commands/project/auth_session/load.py +39 -0
- cli/commands/project/project.py +10 -0
- cli/commands/project/run.py +340 -0
- cli/commands/project/run_interface.py +265 -0
- cli/commands/project/type_check.py +86 -0
- cli/commands/project/upgrade.py +92 -0
- cli/commands/publish_packages.py +264 -0
- cli/logger.py +19 -0
- cli/utils/ai_source_project.py +31 -0
- cli/utils/code_tree.py +83 -0
- cli/utils/run_apis.py +147 -0
- cli/utils/unix_socket.py +55 -0
- intuned_runtime-1.0.0.dist-info/LICENSE +42 -0
- intuned_runtime-1.0.0.dist-info/METADATA +113 -0
- intuned_runtime-1.0.0.dist-info/RECORD +58 -0
- intuned_runtime-1.0.0.dist-info/WHEEL +4 -0
- intuned_runtime-1.0.0.dist-info/entry_points.txt +3 -0
- runtime/__init__.py +3 -0
- runtime/backend_functions/__init__.py +5 -0
- runtime/backend_functions/_call_backend_function.py +86 -0
- runtime/backend_functions/get_auth_session_parameters.py +30 -0
- runtime/browser/__init__.py +3 -0
- runtime/browser/launch_chromium.py +212 -0
- runtime/browser/storage_state.py +106 -0
- runtime/context/__init__.py +5 -0
- runtime/context/context.py +51 -0
- runtime/env.py +13 -0
- runtime/errors/__init__.py +21 -0
- runtime/errors/auth_session_errors.py +9 -0
- runtime/errors/run_api_errors.py +120 -0
- runtime/errors/trace_errors.py +3 -0
- runtime/helpers/__init__.py +5 -0
- runtime/helpers/extend_payload.py +9 -0
- runtime/helpers/extend_timeout.py +13 -0
- runtime/helpers/get_auth_session_parameters.py +14 -0
- runtime/py.typed +0 -0
- runtime/run/__init__.py +3 -0
- runtime/run/intuned_settings.py +38 -0
- runtime/run/playwright_constructs.py +19 -0
- runtime/run/run_api.py +233 -0
- runtime/run/traces.py +36 -0
- runtime/types/__init__.py +15 -0
- runtime/types/payload.py +7 -0
- runtime/types/run_types.py +177 -0
- runtime_helpers/__init__.py +5 -0
- 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,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,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,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,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
|
runtime/run/__init__.py
ADDED
@@ -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()
|