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
runtime/run/run_api.py
CHANGED
@@ -1,21 +1,18 @@
|
|
1
1
|
import asyncio
|
2
2
|
import functools
|
3
3
|
import json
|
4
|
-
import
|
4
|
+
import sys
|
5
|
+
from collections.abc import Callable
|
6
|
+
from contextlib import AsyncExitStack
|
5
7
|
from importlib import import_module
|
6
|
-
from importlib.util import module_from_spec
|
7
|
-
from importlib.util import spec_from_file_location
|
8
8
|
from inspect import iscoroutinefunction
|
9
9
|
from typing import Any
|
10
|
-
from typing import
|
11
|
-
from typing import
|
10
|
+
from typing import Awaitable
|
11
|
+
from typing import cast
|
12
12
|
from typing import Optional
|
13
13
|
from typing import Protocol
|
14
14
|
|
15
|
-
from
|
16
|
-
from tenacity import retry_if_not_result
|
17
|
-
from tenacity import RetryError
|
18
|
-
from tenacity import stop_after_attempt
|
15
|
+
from git import TYPE_CHECKING
|
19
16
|
|
20
17
|
from runtime.browser.storage_state import get_storage_state
|
21
18
|
from runtime.browser.storage_state import set_storage_state
|
@@ -23,6 +20,7 @@ from runtime.context import IntunedContext
|
|
23
20
|
from runtime.errors.run_api_errors import InvalidSessionError
|
24
21
|
from runtime.errors.run_api_errors import ResultTooBigError
|
25
22
|
from runtime.errors.run_api_errors import RunApiError
|
23
|
+
from runtime.run.playwright_context import playwright_context
|
26
24
|
from runtime.types import RunAutomationSuccessResult
|
27
25
|
from runtime.types.run_types import PayloadToAppend
|
28
26
|
from runtime.types.run_types import RunApiParameters
|
@@ -31,8 +29,12 @@ from ..errors import ApiNotFoundError
|
|
31
29
|
from ..errors import AutomationError
|
32
30
|
from ..errors import AutomationNotCoroutineError
|
33
31
|
from ..errors import NoAutomationInApiError
|
34
|
-
from .
|
32
|
+
from .playwright_tracing import playwright_tracing
|
35
33
|
from .pydantic_encoder import PydanticEncoder
|
34
|
+
from .types import ImportFunction
|
35
|
+
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
from playwright.async_api import Page
|
36
38
|
|
37
39
|
|
38
40
|
def get_object_size_in_bytes(obj: Any) -> int:
|
@@ -49,31 +51,20 @@ def import_function_from_api_dir(
|
|
49
51
|
file_path: str,
|
50
52
|
base_dir: Optional[str] = None,
|
51
53
|
automation_function_name: str | None = None,
|
52
|
-
) -> Callable[...,
|
54
|
+
) -> Callable[..., Awaitable[Any]]:
|
53
55
|
module_path = file_path.replace("/", ".")
|
54
56
|
|
55
57
|
def _import_module():
|
56
|
-
if base_dir is None:
|
57
|
-
|
58
|
-
|
59
|
-
file_location = os.path.join(base_dir, f"{file_path}.py")
|
60
|
-
if not os.path.exists(file_location):
|
61
|
-
raise ApiNotFoundError(module_path)
|
62
|
-
spec = spec_from_file_location(os.path.basename(file_path), file_location)
|
63
|
-
if spec is None:
|
64
|
-
raise ApiNotFoundError(module_path)
|
65
|
-
module = module_from_spec(spec)
|
66
|
-
if spec.loader is None:
|
67
|
-
raise ApiNotFoundError(module_path)
|
68
|
-
spec.loader.exec_module(module)
|
69
|
-
return module
|
58
|
+
if base_dir is not None:
|
59
|
+
sys.path.insert(0, base_dir)
|
60
|
+
return import_module(module_path)
|
70
61
|
|
71
62
|
try:
|
72
63
|
module = _import_module()
|
73
64
|
|
74
65
|
except ModuleNotFoundError as e:
|
75
66
|
# if the top-level module does not exist, it is a 404
|
76
|
-
if e.name == module_path:
|
67
|
+
if e.name == module_path or e.name == module_path.split(".", 1)[0]:
|
77
68
|
raise ApiNotFoundError(module_path) from e
|
78
69
|
|
79
70
|
# otherwise, it is an import error inside the user code
|
@@ -113,8 +104,10 @@ def import_function_from_api_dir(
|
|
113
104
|
return automation_coroutine
|
114
105
|
|
115
106
|
|
116
|
-
class
|
117
|
-
def __call__(
|
107
|
+
class AutomationFunction(Protocol):
|
108
|
+
def __call__(
|
109
|
+
self, page: "Page", parameters: dict[Any, Any] | None = None, *args: Any, **kwargs: Any
|
110
|
+
) -> Awaitable[Any]: ...
|
118
111
|
|
119
112
|
|
120
113
|
async def run_api(
|
@@ -124,30 +117,50 @@ async def run_api(
|
|
124
117
|
) -> RunAutomationSuccessResult:
|
125
118
|
from playwright.async_api import ProxySettings
|
126
119
|
|
127
|
-
|
120
|
+
import_function = import_function or (
|
121
|
+
lambda file_path, name=None: import_function_from_api_dir(file_path=file_path, automation_function_name=name)
|
122
|
+
)
|
128
123
|
|
129
|
-
|
130
|
-
|
131
|
-
|
124
|
+
async with AsyncExitStack() as stack:
|
125
|
+
_initialize_playwright_context = functools.partial(
|
126
|
+
playwright_context,
|
127
|
+
import_function=import_function,
|
128
|
+
api_name=parameters.automation_function.name,
|
129
|
+
api_parameters=parameters.automation_function.params,
|
130
|
+
)
|
131
|
+
if parameters.run_options.environment == "standalone":
|
132
|
+
proxy_config = parameters.run_options.proxy
|
133
|
+
if proxy_config is not None:
|
134
|
+
proxy = ProxySettings(
|
135
|
+
**proxy_config.model_dump(by_alias=True),
|
136
|
+
)
|
137
|
+
else:
|
138
|
+
proxy = None
|
132
139
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
140
|
+
context, page = await stack.enter_async_context(
|
141
|
+
_initialize_playwright_context(
|
142
|
+
proxy=proxy,
|
143
|
+
headless=parameters.run_options.headless,
|
144
|
+
)
|
145
|
+
)
|
146
|
+
|
147
|
+
else:
|
148
|
+
context, page = await stack.enter_async_context(
|
149
|
+
_initialize_playwright_context(
|
150
|
+
cdp_address=parameters.run_options.cdp_address,
|
151
|
+
)
|
139
152
|
)
|
140
|
-
else:
|
141
|
-
cdp_address = parameters.run_options.cdp_address
|
142
153
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
154
|
+
if parameters.tracing.enabled is True:
|
155
|
+
await stack.enter_async_context(
|
156
|
+
playwright_tracing(
|
157
|
+
context=context,
|
158
|
+
trace_path=parameters.tracing.file_path,
|
159
|
+
screenshots=True,
|
160
|
+
snapshots=True,
|
161
|
+
sources=True,
|
162
|
+
)
|
163
|
+
)
|
151
164
|
|
152
165
|
if parameters.auth is not None and parameters.auth.session.type == "state":
|
153
166
|
if parameters.auth.session.state is None:
|
@@ -157,40 +170,16 @@ async def run_api(
|
|
157
170
|
context=context,
|
158
171
|
state=state,
|
159
172
|
)
|
160
|
-
import_function = import_function or (
|
161
|
-
lambda file_path, name=None: import_function_from_api_dir(
|
162
|
-
file_path=file_path, automation_function_name=name
|
163
|
-
)
|
164
|
-
)
|
165
173
|
|
166
174
|
async def _run_automation():
|
167
175
|
try:
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
retry_configs = retry(
|
172
|
-
stop=stop_after_attempt(2),
|
173
|
-
retry=retry_if_not_result(lambda result: result is True),
|
174
|
-
reraise=True,
|
175
|
-
)
|
176
|
-
|
177
|
-
check_fn = import_function("auth-sessions/check")
|
178
|
-
|
179
|
-
check_fn_with_retries = retry_configs(check_fn)
|
180
|
-
try:
|
181
|
-
check_result = await check_fn_with_retries(page)
|
182
|
-
except RetryError:
|
183
|
-
check_result = False
|
184
|
-
if type(check_result) is not bool:
|
185
|
-
raise AutomationError(TypeError("Check function must return a boolean"))
|
186
|
-
if not check_result:
|
187
|
-
raise InvalidSessionError()
|
188
|
-
|
189
|
-
automation_coroutine_with_page = functools.partial(automation_coroutine, page)
|
176
|
+
automation_function = cast(AutomationFunction, import_function(parameters.automation_function.name))
|
177
|
+
|
178
|
+
automation_function = functools.partial(automation_function, page)
|
190
179
|
if parameters.automation_function.params is None:
|
191
|
-
automation_result = await
|
180
|
+
automation_result = await automation_function()
|
192
181
|
else:
|
193
|
-
automation_result = await
|
182
|
+
automation_result = await automation_function(parameters.automation_function.params)
|
194
183
|
try:
|
195
184
|
automation_result = json.loads(json.dumps(automation_result, cls=PydanticEncoder))
|
196
185
|
except TypeError as e:
|
@@ -228,16 +217,13 @@ async def run_api(
|
|
228
217
|
# Get all public attributes of the exception
|
229
218
|
raise AutomationError(e) from e
|
230
219
|
|
231
|
-
automation_task =
|
220
|
+
automation_task = asyncio.create_task(_run_automation())
|
232
221
|
try:
|
233
|
-
automation_task = asyncio.create_task(_run_automation())
|
234
|
-
|
235
222
|
# Shield will make the CancelledError get thrown directly here instead of inside `automation_task`
|
236
|
-
|
237
|
-
return result
|
223
|
+
return await asyncio.shield(automation_task)
|
238
224
|
except asyncio.CancelledError:
|
239
225
|
# Manually cancel the automation task
|
240
|
-
if
|
226
|
+
if not automation_task.done():
|
241
227
|
automation_task.cancel()
|
242
228
|
try:
|
243
229
|
# Wait for the automation task to be cancelled for a brief moment
|
@@ -245,11 +231,5 @@ async def run_api(
|
|
245
231
|
except (asyncio.CancelledError, asyncio.TimeoutError):
|
246
232
|
pass
|
247
233
|
raise # Re-raise the cancellation
|
248
|
-
|
249
|
-
|
250
|
-
try:
|
251
|
-
await context.tracing.stop(path=parameters.tracing.file_path)
|
252
|
-
except Exception as e:
|
253
|
-
print("Error stopping tracing:", e)
|
254
|
-
os.remove(parameters.tracing.file_path)
|
255
|
-
await context.close()
|
234
|
+
|
235
|
+
raise RuntimeError("Unreachable code path")
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
from typing import Awaitable
|
4
|
+
from typing import cast
|
5
|
+
from typing import Protocol
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
from typing import Union
|
8
|
+
|
9
|
+
from runtime.errors.run_api_errors import ApiNotFoundError
|
10
|
+
|
11
|
+
from .types import ImportFunction
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from playwright.async_api import BrowserContext
|
15
|
+
from playwright.async_api import Page
|
16
|
+
|
17
|
+
|
18
|
+
setup_context_hook_path = "hooks/setup_context"
|
19
|
+
setup_context_hook_function_name = "setup_context"
|
20
|
+
|
21
|
+
SetupContextHookReturn = Union[
|
22
|
+
"None",
|
23
|
+
"BrowserContext",
|
24
|
+
"tuple[BrowserContext, Page | None]",
|
25
|
+
"tuple[BrowserContext, Page | None, Callable[..., Awaitable[None]]]",
|
26
|
+
]
|
27
|
+
|
28
|
+
|
29
|
+
class SetupContextHook(Protocol):
|
30
|
+
def __call__(self, *, cdp_url: str, api_name: str, api_parameters: Any) -> Awaitable[SetupContextHookReturn]: ...
|
31
|
+
|
32
|
+
|
33
|
+
def load_setup_context_hook(*, import_function: ImportFunction):
|
34
|
+
try:
|
35
|
+
setup_context_hook = cast(
|
36
|
+
SetupContextHook, import_function(setup_context_hook_path, setup_context_hook_function_name)
|
37
|
+
)
|
38
|
+
return setup_context_hook
|
39
|
+
except ApiNotFoundError:
|
40
|
+
return None
|
runtime/run/types.py
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
from typing import Awaitable
|
4
|
+
from typing import Optional
|
5
|
+
from typing import Protocol
|
6
|
+
|
7
|
+
from git import TYPE_CHECKING
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
pass
|
11
|
+
|
12
|
+
|
13
|
+
class ImportFunction(Protocol):
|
14
|
+
def __call__(self, file_path: str, name: Optional[str] = None, /) -> Callable[..., Awaitable[Any]]: ...
|
runtime/types/run_types.py
CHANGED
runtime_helpers/__init__.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from runtime.helpers import extend_payload
|
2
2
|
from runtime.helpers import extend_timeout
|
3
|
+
from runtime.helpers.attempt_store import attempt_store
|
3
4
|
from runtime.helpers.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"]
|
@@ -1,20 +0,0 @@
|
|
1
|
-
from contextlib import asynccontextmanager
|
2
|
-
from typing import TYPE_CHECKING
|
3
|
-
|
4
|
-
if TYPE_CHECKING:
|
5
|
-
from playwright.async_api import ProxySettings
|
6
|
-
from ..browser import launch_browser
|
7
|
-
|
8
|
-
|
9
|
-
@asynccontextmanager
|
10
|
-
async def get_production_playwright_constructs(
|
11
|
-
proxy: "ProxySettings | None" = None,
|
12
|
-
headless: bool = False,
|
13
|
-
*,
|
14
|
-
cdp_address: str | None = None,
|
15
|
-
):
|
16
|
-
async with launch_browser(headless=headless, cdp_address=cdp_address, proxy=proxy) as (context, page):
|
17
|
-
try:
|
18
|
-
yield context, page
|
19
|
-
finally:
|
20
|
-
await context.close()
|
File without changes
|
File without changes
|