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.
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.1.dist-info → intuned_runtime-1.2.3.dist-info}/METADATA +4 -2
  23. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.dist-info}/RECORD +43 -36
  24. {intuned_runtime-1.2.1.dist-info → intuned_runtime-1.2.3.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 +27 -57
  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.1.dist-info → intuned_runtime-1.2.3.dist-info}/entry_points.txt +0 -0
  44. {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 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")
@@ -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]]: ...
@@ -126,7 +126,6 @@ class TracingDisabled(CamelBaseModel):
126
126
 
127
127
  class Auth(CamelBaseModel):
128
128
  session: RunApiSession
129
- run_check: bool = False
130
129
 
131
130
 
132
131
  class IntunedRunContext(CamelBaseModel):
@@ -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()