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
intuned_cli/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import sys
2
3
 
3
4
  import arguably
@@ -8,7 +9,7 @@ from intuned_cli.utils.console import console
8
9
  from intuned_cli.utils.error import CLIError
9
10
  from intuned_cli.utils.error import log_automation_error
10
11
  from runtime.context.context import IntunedContext
11
- from runtime.errors.run_api_errors import AutomationError
12
+ from runtime.errors.run_api_errors import RunApiError
12
13
 
13
14
  from . import commands
14
15
 
@@ -17,6 +18,15 @@ def run():
17
18
  dotenv = find_dotenv(usecwd=True)
18
19
  if dotenv:
19
20
  load_dotenv(dotenv, override=True)
21
+ from runtime.env import cli_env_var_key
22
+
23
+ os.environ[cli_env_var_key] = "true"
24
+ os.environ["RUN_ENVIRONMENT"] = "AUTHORING"
25
+
26
+ if not os.environ.get("FUNCTIONS_DOMAIN"):
27
+ from intuned_cli.utils.backend import get_base_url
28
+
29
+ os.environ["FUNCTIONS_DOMAIN"] = get_base_url().replace("/$", "")
20
30
  try:
21
31
  with IntunedContext():
22
32
  arguably.run(name="intuned")
@@ -26,7 +36,7 @@ def run():
26
36
  console.print(f"[bold red]{e.message}[/bold red]")
27
37
  else:
28
38
  console.print(e.message)
29
- except AutomationError as e:
39
+ except RunApiError as e:
30
40
  log_automation_error(e)
31
41
  except KeyboardInterrupt:
32
42
  console.print("[bold red]Aborted[/bold red]")
@@ -16,3 +16,4 @@ from .run_authsession_create_command import run__authsession__create as run__aut
16
16
  from .run_authsession_update_command import run__authsession__update as run__authsession__update # type: ignore
17
17
  from .run_authsession_validate_command import run__authsession__validate as run__authsession__validate # type: ignore
18
18
  from .run_command import run as run # type: ignore
19
+ from .save_command import save as save # type: ignore
@@ -25,7 +25,7 @@ async def attempt__api(
25
25
  parameters (str): Path to JSON file containing API parameters or the parameters as a JSON string.
26
26
  auth_session (str | None, optional): [-a/--auth-session]. ID of the auth session to use for the API. This is expected to be in ./auth-session-instances/<id>
27
27
  proxy (str | None, optional): [--proxy]. Proxy URL to use. Defaults to None.
28
- timeout (str, optional): [--timeout]. Timeout - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
28
+ timeout (str, optional): [--timeout]. Timeout - seconds or pytimeparse-formatted string. Defaults to "10 min".
29
29
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
30
30
  output_file (str | None, optional): [-o/--output-file]. Output file path. Defaults to None.
31
31
  """
@@ -19,7 +19,7 @@ async def attempt__authsession__check(
19
19
  Args:
20
20
  id (str): ID of the auth session to check
21
21
  proxy (str | None, optional): [--proxy]. Proxy URL to use for the auth session command. Defaults to None.
22
- timeout (str, optional): [--timeout]. Timeout for the auth session command - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
22
+ timeout (str, optional): [--timeout]. Timeout for the auth session command - seconds or pytimeparse-formatted string. Defaults to "10 min".
23
23
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
24
24
  """
25
25
  await assert_auth_enabled()
@@ -22,7 +22,7 @@ async def attempt__authsession__create(
22
22
  parameters (str): Parameters for the auth session command
23
23
  id (str | None, optional): [--id]. ID of the auth session to use for the command. Defaults to ./auth-session-instances/[current timestamp].json.
24
24
  proxy (str | None, optional): [--proxy]. Proxy URL to use for the auth session command. Defaults to None.
25
- timeout (str, optional): [--timeout]. Timeout for the auth session command - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
25
+ timeout (str, optional): [--timeout]. Timeout for the auth session command - seconds or pytimeparse-formatted string. Defaults to "10 min".
26
26
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
27
27
  """
28
28
  await assert_auth_enabled()
@@ -1,9 +1,9 @@
1
1
  import arguably
2
2
 
3
3
  from intuned_cli.controller.deploy import deploy_project
4
- from intuned_cli.controller.deploy import get_intuned_api_auth_credentials
5
- from intuned_cli.controller.deploy import validate_intuned_project
6
- from intuned_cli.controller.deploy import validate_project_name
4
+ from intuned_cli.controller.save import validate_intuned_project
5
+ from intuned_cli.controller.save import validate_project_name
6
+ from intuned_cli.utils.backend import get_intuned_api_auth_credentials
7
7
  from intuned_cli.utils.error import CLIError
8
8
 
9
9
 
@@ -15,7 +15,7 @@ async def deploy(
15
15
  workspace_id: str | None = None,
16
16
  api_key: str | None = None,
17
17
  ):
18
- """Deploys the application.
18
+ """Saves and deploys the project to Intuned.
19
19
 
20
20
  Args:
21
21
  project_name (str | None, optional): The name of the project to deploy.
@@ -34,7 +34,7 @@ async def run__api(
34
34
  no_auth_session_auto_recreate (bool, optional): [--no-auth-session-auto-recreate]. Disable auto recreate for auth session. Defaults to False.
35
35
  auth_session_check_attempts (int, optional): [--auth-session-check-attempts]. Auth session check attempts. Defaults to 1.
36
36
  auth_session_create_attempts (int, optional): [--auth-session-create-attempts]. Auth session create attempts. Defaults to 1.
37
- timeout (str, optional): [--timeout]. Timeout - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
37
+ timeout (str, optional): [--timeout]. Timeout - seconds or pytimeparse-formatted string. Defaults to "10 min".
38
38
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
39
39
  output_file (str | None, optional): [-o/--output-file]. Output file path. Defaults to None.
40
40
  """
@@ -26,7 +26,7 @@ async def run__authsession__create(
26
26
  check_attempts (int, optional): [--check-attempts]. Number of attempts to check the auth session validity. Defaults to 1.
27
27
  create_attempts (int, optional): [--create-attempts]. Number of attempts to create a new auth session if it is invalid. Defaults to 1.
28
28
  proxy (str | None, optional): [--proxy]. Proxy URL to use for the auth session command. Defaults to None.
29
- timeout (str, optional): [--timeout]. Timeout for the auth session command - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
29
+ timeout (str, optional): [--timeout]. Timeout for the auth session command - seconds or pytimeparse-formatted string. Defaults to "10 min".
30
30
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
31
31
  """
32
32
  await assert_auth_enabled()
@@ -26,7 +26,7 @@ async def run__authsession__update(
26
26
  check_attempts (int, optional): [--check-attempts]. Number of attempts to check the auth session validity. Defaults to 1.
27
27
  create_attempts (int, optional): [--create-attempts]. Number of attempts to create a new auth session if it is invalid. Defaults to 1.
28
28
  proxy (str | None, optional): [--proxy]. Proxy URL to use for the auth session command. Defaults to None.
29
- timeout (str, optional): [--timeout]. Timeout for the auth session command - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
29
+ timeout (str, optional): [--timeout]. Timeout for the auth session command - seconds or pytimeparse-formatted string. Defaults to "10 min".
30
30
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
31
31
  """
32
32
  await assert_auth_enabled()
@@ -24,7 +24,7 @@ async def run__authsession__validate(
24
24
  check_attempts (int, optional): [--check-attempts]. Number of attempts to check the auth session validity. Defaults to 1.
25
25
  create_attempts (int, optional): [--create-attempts]. Number of attempts to create a new auth session if it is invalid. Defaults to 1.
26
26
  proxy (str | None, optional): [--proxy]. Proxy URL to use for the auth session command. Defaults to None.
27
- timeout (str, optional): [--timeout]. Timeout for the auth session command - milliseconds or pytimeparse-formatted string. Defaults to "10 min".
27
+ timeout (str, optional): [--timeout]. Timeout for the auth session command - seconds or pytimeparse-formatted string. Defaults to "10 min".
28
28
  no_auto_recreate (bool, optional): [--no-auto-recreate]. Disable auto recreation of the auth session if it is invalid. Defaults to False.
29
29
  headless (bool, optional): [--headless]. Run the API in headless mode (default: False). This will not open a browser window.
30
30
  """
@@ -0,0 +1,47 @@
1
+ import arguably
2
+
3
+ from intuned_cli.controller.save import save_project
4
+ from intuned_cli.controller.save import validate_intuned_project
5
+ from intuned_cli.controller.save import validate_project_name
6
+ from intuned_cli.utils.backend import get_intuned_api_auth_credentials
7
+ from intuned_cli.utils.error import CLIError
8
+
9
+
10
+ @arguably.command # type: ignore
11
+ async def save(
12
+ project_name: str | None = None,
13
+ /,
14
+ *,
15
+ workspace_id: str | None = None,
16
+ api_key: str | None = None,
17
+ ):
18
+ """Saves the project to Intuned without deploying it.
19
+
20
+ Args:
21
+ project_name (str | None, optional): The name of the project to save.
22
+ workspace_id (str | None, optional): The ID of the workspace to save to.
23
+ api_key (str | None, optional): The API key to use for authentication.
24
+ """
25
+ try:
26
+ intuned_json = await validate_intuned_project()
27
+ except CLIError as e:
28
+ raise CLIError(
29
+ f"[bold red]Project to be saved is not a valid Intuned project:[/bold red][bright_red] {e}[/bright_red]\n",
30
+ auto_color=False,
31
+ ) from e
32
+
33
+ project_name = project_name or intuned_json.project_name
34
+ if not project_name:
35
+ raise CLIError("Project name is required")
36
+
37
+ validate_project_name(project_name)
38
+
39
+ workspace_id, api_key = await get_intuned_api_auth_credentials(
40
+ intuned_json=intuned_json, workspace_id=workspace_id, api_key=api_key
41
+ )
42
+
43
+ await save_project(
44
+ project_name=project_name,
45
+ workspace_id=workspace_id,
46
+ api_key=api_key,
47
+ )
@@ -187,7 +187,6 @@ class TestAttemptApi:
187
187
  assert call_args.run_options.headless is False
188
188
  assert call_args.run_options.proxy == proxy_config
189
189
  assert call_args.auth.session.state is auth
190
- assert call_args.auth.run_check is False
191
190
 
192
191
  @pytest.mark.asyncio
193
192
  async def test_attempt_api_returns_result_and_extended_payloads_if_run_api_succeeds(
@@ -197,7 +197,6 @@ async def attempt_api(
197
197
  automation_function=AutomationFunction(name=f"api/{api_name}", params=parameters),
198
198
  auth=Auth(
199
199
  session=StateSession(state=auth),
200
- run_check=False,
201
200
  )
202
201
  if auth
203
202
  else None,
@@ -276,7 +276,6 @@ async def run_check(
276
276
  session=StateSession(
277
277
  state=auth,
278
278
  ),
279
- run_check=False,
280
279
  ),
281
280
  ),
282
281
  import_function=await get_cli_import_function(),
@@ -1,29 +1,15 @@
1
1
  import asyncio
2
- import json
3
- import os
4
- import re
5
2
  import time
6
- import uuid
7
3
  from itertools import cycle
8
- from typing import Any
9
4
  from typing import Literal
10
5
 
11
6
  import httpx
12
- import pathspec
13
- import toml
14
- from anyio import Path
15
7
  from pydantic import BaseModel
16
8
 
17
- from intuned_cli.types import DirectoryNode
18
- from intuned_cli.types import FileNode
19
- from intuned_cli.types import FileNodeContent
20
- from intuned_cli.types import FileSystemTree
21
- from intuned_cli.types import IntunedJson
22
- from intuned_cli.utils.api_helpers import load_intuned_json
9
+ from intuned_cli.controller.save import save_project
23
10
  from intuned_cli.utils.backend import get_base_url
24
11
  from intuned_cli.utils.console import console
25
12
  from intuned_cli.utils.error import CLIError
26
- from intuned_cli.utils.exclusions import exclusions
27
13
 
28
14
  supported_playwright_versions = ["1.46.0", "1.52.0"]
29
15
 
@@ -31,189 +17,6 @@ project_deploy_timeout = 10 * 60
31
17
  project_deploy_check_period = 5
32
18
 
33
19
 
34
- class IntunedPyprojectToml(BaseModel):
35
- class _Tool(BaseModel):
36
- class _Poetry(BaseModel):
37
- dependencies: dict[str, Any]
38
-
39
- poetry: _Poetry
40
-
41
- tool: _Tool
42
-
43
-
44
- async def validate_intuned_project():
45
- cwd = await Path().resolve()
46
-
47
- pyproject_toml_path = cwd / "pyproject.toml"
48
-
49
- if not await pyproject_toml_path.exists():
50
- raise CLIError("pyproject.toml file is missing in the current directory.")
51
-
52
- content = await pyproject_toml_path.read_text()
53
- json_content = toml.loads(content)
54
- try:
55
- pyproject_toml = IntunedPyprojectToml.model_validate(json_content)
56
- except Exception as e:
57
- raise CLIError(f"Failed to parse pyproject.toml: {e}") from e
58
-
59
- playwright_version = pyproject_toml.tool.poetry.dependencies.get("playwright")
60
-
61
- if playwright_version not in supported_playwright_versions:
62
- raise CLIError(
63
- f"Unsupported Playwright version '{playwright_version}'. "
64
- f"Supported versions are: {', '.join(supported_playwright_versions)}."
65
- )
66
-
67
- intuned_json = await load_intuned_json()
68
-
69
- api_folder = cwd / "api"
70
- if not await api_folder.exists() or not await api_folder.is_dir():
71
- raise CLIError("api directory does not exist in the current directory.")
72
-
73
- if intuned_json.auth_sessions.enabled:
74
- auth_sessions_folder = cwd / "auth-sessions"
75
- if not await auth_sessions_folder.exists() or not await auth_sessions_folder.is_dir():
76
- raise CLIError("auth-sessions directory does not exist in the api directory.")
77
-
78
- return intuned_json
79
-
80
-
81
- def validate_project_name(project_name: str):
82
- if len(project_name) > 50:
83
- raise CLIError("Project name must be 50 characters or less.")
84
-
85
- project_name_regex = r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$"
86
- if not re.match(project_name_regex, project_name):
87
- raise CLIError("Project name can only contain lowercase letters, numbers, hyphens, and underscores in between.")
88
-
89
- try:
90
- import uuid
91
-
92
- uuid.UUID(project_name)
93
- raise CLIError("Project name cannot be a UUID.")
94
- except ValueError:
95
- # Not a valid UUID, continue
96
- pass
97
-
98
-
99
- async def get_intuned_api_auth_credentials(
100
- *, intuned_json: IntunedJson, workspace_id: str | None, api_key: str | None
101
- ) -> tuple[str, str]:
102
- """
103
- Retrieves the Intuned API authentication credentials from environment variables.
104
-
105
- Returns:
106
- tuple: A tuple containing the workspace ID and API key.
107
- """
108
- workspace_id = workspace_id or intuned_json.workspace_id
109
- api_key = api_key or os.environ.get("INTUNED_API_KEY")
110
-
111
- if not workspace_id:
112
- raise CLIError("Workspace ID is required. Please provide it via command line options or Intuned.json")
113
-
114
- if not api_key:
115
- raise CLIError(
116
- "API key is required. Please provide it via command line options or INTUNED_API_KEY environment variable."
117
- )
118
-
119
- return workspace_id, api_key
120
-
121
-
122
- async def get_file_tree_from_project(path: Path, *, exclude: list[str] | None = None):
123
- # Create pathspec object for gitignore-style pattern matching
124
- spec = None
125
- if exclude:
126
- spec = pathspec.PathSpec.from_lines("gitwildmatch", exclude)
127
-
128
- async def traverse(current_path: Path, tree: FileSystemTree):
129
- async for entry in current_path.iterdir():
130
- relative_path_name = entry.relative_to(path).as_posix()
131
- basename = entry.name
132
-
133
- # Check if this path should be excluded
134
- if spec and spec.match_file(relative_path_name):
135
- continue
136
-
137
- if await entry.is_dir():
138
- subtree = FileSystemTree(root={})
139
- tree.root[basename] = DirectoryNode(directory=subtree)
140
- # For directories, check if the directory itself is excluded
141
- # If not excluded, traverse into it
142
- await traverse(entry, subtree)
143
- elif await entry.is_file():
144
- tree.root[basename] = FileNode(file=FileNodeContent(contents=await entry.read_text()))
145
-
146
- results = FileSystemTree(root={})
147
- await traverse(path, results)
148
- return results
149
-
150
-
151
- def mapFileTreeToIdeFileTree(file_tree: FileSystemTree):
152
- """
153
- Maps the file tree to IDE parameters format by processing parameters directory
154
- and converting it to ____testParameters structure.
155
- """
156
-
157
- if not file_tree:
158
- return
159
-
160
- parameters_node = file_tree.root.get("parameters")
161
- if parameters_node is None:
162
- return
163
-
164
- if not isinstance(parameters_node, DirectoryNode):
165
- return
166
-
167
- api_parameters_map: dict[str, list[dict[str, Any]]] = {}
168
- cli_parameters = list(parameters_node.directory.root.keys())
169
- test_parameters = DirectoryNode(directory=FileSystemTree(root={}))
170
-
171
- for parameter_key in cli_parameters:
172
- # If parameter of type directory, discard it and continue
173
- parameter = parameters_node.directory.root[parameter_key]
174
-
175
- if isinstance(parameter, DirectoryNode):
176
- continue
177
-
178
- if not parameter.file.contents.strip():
179
- continue
180
-
181
- try:
182
- parameter_payload = json.loads(parameter.file.contents)
183
- except json.JSONDecodeError:
184
- continue
185
-
186
- if "__api-name" not in parameter_payload:
187
- continue
188
-
189
- api = parameter_payload["__api-name"]
190
- # Create parameter value by excluding __api-name
191
- parameter_value = {k: v for k, v in parameter_payload.items() if k != "__api-name"}
192
-
193
- test_parameter: dict[str, Any] = {
194
- "name": parameter_key.replace(".json", ""),
195
- "lastUsed": False,
196
- "id": str(uuid.uuid4()),
197
- "value": json.dumps(parameter_value),
198
- }
199
-
200
- if api not in api_parameters_map:
201
- api_parameters_map[api] = []
202
- api_parameters_map[api].append(test_parameter)
203
-
204
- for api, parameters in api_parameters_map.items():
205
- # By default, last one used is the last one in the map
206
- if len(parameters) > 0:
207
- parameters[-1]["lastUsed"] = True
208
-
209
- test_parameters.directory.root[f"{api}.json"] = FileNode(
210
- file=FileNodeContent(contents=json.dumps(parameters, indent=2))
211
- )
212
-
213
- del file_tree.root["parameters"]
214
- file_tree.root["____testParameters"] = test_parameters
215
-
216
-
217
20
  class DeployStatus(BaseModel):
218
21
  status: Literal["completed", "failed", "pending"]
219
22
  message: str | None = None
@@ -257,25 +60,20 @@ async def deploy_project(
257
60
  workspace_id: str,
258
61
  api_key: str,
259
62
  ):
63
+ await save_project(
64
+ project_name=project_name,
65
+ workspace_id=workspace_id,
66
+ api_key=api_key,
67
+ )
260
68
  base_url = get_base_url()
261
- url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/create"
69
+ url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/{project_name}/deploy"
262
70
  headers = {
263
71
  "x-api-key": api_key,
264
72
  "Content-Type": "application/json",
265
73
  }
266
- cwd = await Path().resolve()
267
- file_tree = await get_file_tree_from_project(cwd, exclude=exclusions)
268
- mapFileTreeToIdeFileTree(file_tree)
269
-
270
- payload: dict[str, Any] = {
271
- "name": project_name,
272
- "codeTree": file_tree.model_dump(mode="json"),
273
- "isCli": True,
274
- "language": "python",
275
- }
276
74
 
277
75
  async with httpx.AsyncClient() as client:
278
- response = await client.post(url, headers=headers, json=payload)
76
+ response = await client.post(url, headers=headers)
279
77
  if response.status_code < 200 or response.status_code >= 300:
280
78
  if response.status_code == 401:
281
79
  raise CLIError("Invalid API key. Please check your API key and try again.")