intuned-runtime 1.3.1__py3-none-any.whl → 1.3.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.

Potentially problematic release.


This version of intuned-runtime might be problematic. Click here for more details.

Files changed (69) hide show
  1. intuned_cli/__init__.py +15 -24
  2. intuned_cli/commands/__init__.py +5 -0
  3. intuned_cli/commands/attempt_api_command.py +8 -2
  4. intuned_cli/commands/attempt_authsession_check_command.py +8 -2
  5. intuned_cli/commands/attempt_authsession_command.py +4 -7
  6. intuned_cli/commands/attempt_authsession_create_command.py +9 -3
  7. intuned_cli/commands/attempt_command.py +4 -7
  8. intuned_cli/commands/authsession_command.py +9 -0
  9. intuned_cli/commands/authsession_record_command.py +52 -0
  10. intuned_cli/commands/command.py +6 -7
  11. intuned_cli/commands/deploy_command.py +2 -3
  12. intuned_cli/commands/init_command.py +2 -3
  13. intuned_cli/commands/run_api_command.py +9 -3
  14. intuned_cli/commands/run_authsession_command.py +4 -7
  15. intuned_cli/commands/run_authsession_create_command.py +32 -5
  16. intuned_cli/commands/run_authsession_update_command.py +31 -5
  17. intuned_cli/commands/run_authsession_validate_command.py +30 -4
  18. intuned_cli/commands/run_command.py +4 -7
  19. intuned_cli/commands/save_command.py +2 -3
  20. intuned_cli/controller/__test__/test_api.py +159 -18
  21. intuned_cli/controller/__test__/test_authsession.py +497 -6
  22. intuned_cli/controller/api.py +40 -39
  23. intuned_cli/controller/authsession.py +213 -110
  24. intuned_cli/controller/deploy.py +3 -5
  25. intuned_cli/controller/save.py +47 -66
  26. intuned_cli/types.py +14 -0
  27. intuned_cli/utils/__test__/test_browser.py +132 -0
  28. intuned_cli/utils/__test__/test_traces.py +27 -0
  29. intuned_cli/utils/api_helpers.py +54 -5
  30. intuned_cli/utils/auth_session_helpers.py +42 -7
  31. intuned_cli/utils/backend.py +4 -1
  32. intuned_cli/utils/browser.py +63 -0
  33. intuned_cli/utils/error.py +14 -0
  34. intuned_cli/utils/exclusions.py +1 -0
  35. intuned_cli/utils/help.py +9 -0
  36. intuned_cli/utils/traces.py +31 -0
  37. intuned_cli/utils/wrapper.py +68 -0
  38. intuned_internal_cli/__init__.py +7 -0
  39. intuned_internal_cli/commands/__init__.py +8 -16
  40. intuned_internal_cli/commands/browser/__init__.py +1 -1
  41. intuned_internal_cli/commands/browser/save_state.py +2 -3
  42. intuned_internal_cli/commands/project/__init__.py +7 -9
  43. intuned_internal_cli/commands/project/auth_session/__init__.py +3 -3
  44. intuned_internal_cli/commands/project/auth_session/check.py +2 -2
  45. intuned_internal_cli/commands/project/auth_session/create.py +2 -3
  46. intuned_internal_cli/commands/project/auth_session/load.py +2 -3
  47. intuned_internal_cli/commands/project/project.py +2 -2
  48. intuned_internal_cli/commands/project/run.py +2 -2
  49. intuned_internal_cli/commands/project/run_interface.py +75 -8
  50. intuned_internal_cli/commands/project/type_check.py +5 -5
  51. intuned_internal_cli/commands/root.py +2 -1
  52. intuned_internal_cli/utils/wrapper.py +15 -0
  53. {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.3.dist-info}/METADATA +4 -2
  54. intuned_runtime-1.3.3.dist-info/RECORD +115 -0
  55. runtime/browser/launch_browser.py +15 -0
  56. runtime/browser/launch_chromium.py +14 -1
  57. runtime/run/types.py +0 -5
  58. runtime/types/settings_types.py +13 -4
  59. runtime/utils/anyio.py +26 -0
  60. intuned_internal_cli/commands/ai_source/__init__.py +0 -4
  61. intuned_internal_cli/commands/ai_source/ai_source.py +0 -10
  62. intuned_internal_cli/commands/ai_source/deploy.py +0 -64
  63. intuned_internal_cli/commands/init.py +0 -127
  64. intuned_internal_cli/commands/project/upgrade.py +0 -92
  65. intuned_internal_cli/commands/publish_packages.py +0 -264
  66. intuned_runtime-1.3.1.dist-info/RECORD +0 -111
  67. {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.3.dist-info}/WHEEL +0 -0
  68. {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.3.dist-info}/entry_points.txt +0 -0
  69. {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -11,8 +11,6 @@ from intuned_cli.utils.backend import get_base_url
11
11
  from intuned_cli.utils.console import console
12
12
  from intuned_cli.utils.error import CLIError
13
13
 
14
- supported_playwright_versions = ["1.46.0", "1.52.0"]
15
-
16
14
  project_deploy_timeout = 10 * 60
17
15
  project_deploy_check_period = 5
18
16
 
@@ -30,7 +28,7 @@ async def check_deploy_status(
30
28
  api_key: str,
31
29
  ):
32
30
  base_url = get_base_url()
33
- url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/create/{project_name}/result"
31
+ url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/{project_name}/deploy/result"
34
32
 
35
33
  headers = {
36
34
  "x-api-key": api_key,
@@ -124,7 +122,7 @@ async def deploy_project(
124
122
  if update_console_task:
125
123
  update_console_task.cancel()
126
124
  if console.is_terminal:
127
- print("\r", " " * 100)
125
+ print("\r", " " * 100, file=console.file)
128
126
  console.print("[green][bold]Project deployed successfully![/bold][/green]")
129
127
  console.print(
130
128
  f"[bold]You can check your project on the platform:[/bold] [cyan underline]{get_base_url()}/projects/{project_name}/details[/cyan underline]"
@@ -143,7 +141,7 @@ async def deploy_project(
143
141
  )
144
142
  except Exception:
145
143
  if console.is_terminal:
146
- print("\r", " " * 100)
144
+ print("\r", " " * 100, file=console.file)
147
145
  raise
148
146
  finally:
149
147
  if update_console_task:
@@ -6,7 +6,6 @@ from typing import Any
6
6
 
7
7
  import httpx
8
8
  import pathspec
9
- import toml
10
9
  from anyio import Path
11
10
  from dotenv.main import DotEnv
12
11
  from pydantic import BaseModel
@@ -16,6 +15,7 @@ from intuned_cli.types import DirectoryNode
16
15
  from intuned_cli.types import FileNode
17
16
  from intuned_cli.types import FileNodeContent
18
17
  from intuned_cli.types import FileSystemTree
18
+ from intuned_cli.utils.api_helpers import get_intuned_settings_file
19
19
  from intuned_cli.utils.api_helpers import load_intuned_json
20
20
  from intuned_cli.utils.backend import get_base_url
21
21
  from intuned_cli.utils.console import console
@@ -26,8 +26,6 @@ from runtime.env import api_key_env_var_key
26
26
  from runtime.env import project_env_var_key
27
27
  from runtime.env import workspace_env_var_key
28
28
 
29
- supported_playwright_versions = ["1.46.0", "1.52.0"]
30
-
31
29
 
32
30
  class IntunedPyprojectToml(BaseModel):
33
31
  class _Tool(BaseModel):
@@ -47,21 +45,6 @@ async def validate_intuned_project():
47
45
  if not await pyproject_toml_path.exists():
48
46
  raise CLIError("pyproject.toml file is missing in the current directory.")
49
47
 
50
- content = await pyproject_toml_path.read_text()
51
- json_content = toml.loads(content)
52
- try:
53
- pyproject_toml = IntunedPyprojectToml.model_validate(json_content)
54
- except Exception as e:
55
- raise CLIError(f"Failed to parse pyproject.toml: {e}") from e
56
-
57
- playwright_version = pyproject_toml.tool.poetry.dependencies.get("playwright")
58
-
59
- if playwright_version not in supported_playwright_versions:
60
- raise CLIError(
61
- f"Unsupported Playwright version '{playwright_version}'. "
62
- f"Supported versions are: {', '.join(supported_playwright_versions)}."
63
- )
64
-
65
48
  intuned_json = await load_intuned_json()
66
49
 
67
50
  api_folder = cwd / "api"
@@ -123,70 +106,69 @@ async def get_file_tree_from_project(path: Path, *, exclude: list[str] | None =
123
106
  return results
124
107
 
125
108
 
126
- def mapFileTreeToIdeFileTree(file_tree: FileSystemTree):
109
+ async def map_file_tree_to_ide_file_tree(file_tree: FileSystemTree):
127
110
  """
128
111
  Maps the file tree to IDE parameters format by processing parameters directory
129
112
  and converting it to ____testParameters structure.
130
113
  """
131
114
 
132
- if not file_tree:
133
- return
134
-
135
115
  parameters_node = file_tree.root.get("parameters")
136
- if parameters_node is None:
137
- return
116
+ if isinstance(parameters_node, DirectoryNode):
117
+ api_parameters_map: dict[str, list[dict[str, Any]]] = {}
118
+ cli_parameters = list(parameters_node.directory.root.keys())
119
+ test_parameters = DirectoryNode(directory=FileSystemTree(root={}))
138
120
 
139
- if not isinstance(parameters_node, DirectoryNode):
140
- return
141
-
142
- api_parameters_map: dict[str, list[dict[str, Any]]] = {}
143
- cli_parameters = list(parameters_node.directory.root.keys())
144
- test_parameters = DirectoryNode(directory=FileSystemTree(root={}))
121
+ for parameter_key in cli_parameters:
122
+ # If parameter of type directory, discard it and continue
123
+ parameter = parameters_node.directory.root[parameter_key]
145
124
 
146
- for parameter_key in cli_parameters:
147
- # If parameter of type directory, discard it and continue
148
- parameter = parameters_node.directory.root[parameter_key]
125
+ if isinstance(parameter, DirectoryNode):
126
+ continue
149
127
 
150
- if isinstance(parameter, DirectoryNode):
151
- continue
128
+ if not parameter.file.contents.strip():
129
+ continue
152
130
 
153
- if not parameter.file.contents.strip():
154
- continue
131
+ try:
132
+ parameter_payload = json.loads(parameter.file.contents)
133
+ except json.JSONDecodeError:
134
+ continue
155
135
 
156
- try:
157
- parameter_payload = json.loads(parameter.file.contents)
158
- except json.JSONDecodeError:
159
- continue
136
+ if "__api-name" not in parameter_payload:
137
+ continue
160
138
 
161
- if "__api-name" not in parameter_payload:
162
- continue
139
+ api = parameter_payload["__api-name"]
140
+ # Create parameter value by excluding __api-name
141
+ parameter_value = {k: v for k, v in parameter_payload.items() if k != "__api-name"}
163
142
 
164
- api = parameter_payload["__api-name"]
165
- # Create parameter value by excluding __api-name
166
- parameter_value = {k: v for k, v in parameter_payload.items() if k != "__api-name"}
143
+ test_parameter: dict[str, Any] = {
144
+ "name": parameter_key.replace(".json", ""),
145
+ "lastUsed": False,
146
+ "id": str(uuid.uuid4()),
147
+ "value": json.dumps(parameter_value),
148
+ }
167
149
 
168
- test_parameter: dict[str, Any] = {
169
- "name": parameter_key.replace(".json", ""),
170
- "lastUsed": False,
171
- "id": str(uuid.uuid4()),
172
- "value": json.dumps(parameter_value),
173
- }
150
+ if api not in api_parameters_map:
151
+ api_parameters_map[api] = []
152
+ api_parameters_map[api].append(test_parameter)
174
153
 
175
- if api not in api_parameters_map:
176
- api_parameters_map[api] = []
177
- api_parameters_map[api].append(test_parameter)
154
+ for api, parameters in api_parameters_map.items():
155
+ # By default, last one used is the last one in the map
156
+ if len(parameters) > 0:
157
+ parameters[-1]["lastUsed"] = True
178
158
 
179
- for api, parameters in api_parameters_map.items():
180
- # By default, last one used is the last one in the map
181
- if len(parameters) > 0:
182
- parameters[-1]["lastUsed"] = True
159
+ test_parameters.directory.root[f"{api}.json"] = FileNode(
160
+ file=FileNodeContent(contents=json.dumps(parameters, indent=2))
161
+ )
183
162
 
184
- test_parameters.directory.root[f"{api}.json"] = FileNode(
185
- file=FileNodeContent(contents=json.dumps(parameters, indent=2))
186
- )
163
+ del file_tree.root["parameters"]
164
+ file_tree.root["____testParameters"] = test_parameters
187
165
 
188
- del file_tree.root["parameters"]
189
- file_tree.root["____testParameters"] = test_parameters
166
+ if file_tree.root.get("Intuned.json") is None:
167
+ settings_file = await get_intuned_settings_file()
168
+ text_content = await Path(settings_file.file_path).read_text()
169
+ parsed_content = settings_file.parse(text_content)
170
+ json_content = json.dumps(parsed_content, indent=2)
171
+ file_tree.root["Intuned.json"] = FileNode(file=FileNodeContent(contents=json_content))
190
172
 
191
173
 
192
174
  class SaveProjectResponse(BaseModel):
@@ -201,14 +183,13 @@ async def save_project(
201
183
  ):
202
184
  base_url = get_base_url()
203
185
  url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/{project_name}"
204
- print(f"calling {url}")
205
186
  headers = {
206
187
  api_key_header_name: api_key,
207
188
  "Content-Type": "application/json",
208
189
  }
209
190
  cwd = await Path().resolve()
210
191
  file_tree = await get_file_tree_from_project(cwd, exclude=exclusions)
211
- mapFileTreeToIdeFileTree(file_tree)
192
+ await map_file_tree_to_ide_file_tree(file_tree)
212
193
 
213
194
  payload: dict[str, Any] = {
214
195
  "codeTree": file_tree.model_dump(mode="json"),
intuned_cli/types.py CHANGED
@@ -1,3 +1,6 @@
1
+ from typing import NotRequired
2
+ from typing import TypedDict
3
+
1
4
  from pydantic import BaseModel
2
5
  from pydantic import RootModel
3
6
 
@@ -19,3 +22,14 @@ class FileNode(BaseModel):
19
22
 
20
23
 
21
24
  FileSystemTree.model_rebuild()
25
+
26
+
27
+ class BaseExecuteCommandOptionsWithoutTrace(TypedDict):
28
+ headless: bool
29
+ timeout: float
30
+ proxy: NotRequired[str | None]
31
+ keep_browser_open: bool
32
+
33
+
34
+ class BaseExecuteCommandOptions(BaseExecuteCommandOptionsWithoutTrace):
35
+ trace: bool
@@ -0,0 +1,132 @@
1
+ from dataclasses import dataclass
2
+ from unittest.mock import AsyncMock
3
+ from unittest.mock import MagicMock
4
+ from unittest.mock import Mock
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+ import pytest_asyncio
9
+
10
+ from intuned_cli.utils.browser import close_cli_browser
11
+ from intuned_cli.utils.browser import get_cli_run_options
12
+ from runtime.types.run_types import CDPRunOptions
13
+ from runtime.types.run_types import StandaloneRunOptions
14
+
15
+
16
+ @dataclass
17
+ class BrowserMocks:
18
+ launch_chromium: AsyncMock
19
+
20
+
21
+ @pytest_asyncio.fixture
22
+ async def browser_mocks():
23
+ """Mock dependencies for execute_record_auth_session_cli tests."""
24
+
25
+ _mock_launch_chromium = patch("runtime.browser.launch_chromium.launch_chromium")
26
+ _mock_get_free_port = patch("runtime.run.playwright_context.get_random_free_port", new_callable=AsyncMock)
27
+
28
+ with (
29
+ _mock_launch_chromium as mock_launch_chromium,
30
+ _mock_get_free_port as _mock_get_free_port,
31
+ ):
32
+ mock_launch_chromium_return_value = Mock()
33
+
34
+ async def create_context_and_page():
35
+ return (MagicMock(), MagicMock())
36
+
37
+ mock_launch_chromium_return_value.__aenter__ = AsyncMock(side_effect=create_context_and_page)
38
+ mock_launch_chromium_return_value.__aexit__ = AsyncMock(return_value=None)
39
+ mock_launch_chromium.return_value = mock_launch_chromium_return_value
40
+
41
+ _mock_get_free_port.return_value = 1234
42
+
43
+ yield BrowserMocks(
44
+ launch_chromium=mock_launch_chromium,
45
+ )
46
+
47
+ await close_cli_browser()
48
+
49
+
50
+ class TestBrowser:
51
+ @pytest.mark.asyncio
52
+ async def test_returns_standalone_options_if_keep_browser_open_is_false(self, browser_mocks: BrowserMocks):
53
+ from intuned_cli.utils.browser import get_cli_run_options
54
+
55
+ options = await get_cli_run_options(
56
+ keep_browser_open=False,
57
+ headless=False,
58
+ )
59
+ assert options.environment == "standalone"
60
+ assert type(options) is StandaloneRunOptions
61
+ assert options.headless is False
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_launches_browser_and_returns_cdp_options_if_keep_browser_open_is_true(
65
+ self, browser_mocks: BrowserMocks
66
+ ):
67
+ options = await get_cli_run_options(
68
+ keep_browser_open=True,
69
+ headless=False,
70
+ )
71
+ assert options.environment == "cdp"
72
+ assert type(options) is CDPRunOptions
73
+ assert options.cdp_address == "http://localhost:1234"
74
+
75
+ browser_mocks.launch_chromium.assert_called_with(
76
+ headless=False,
77
+ cdp_port=1234,
78
+ proxy=None,
79
+ )
80
+
81
+ @pytest.mark.asyncio
82
+ async def test_keeps_context_open_until_new_context_if_keep_browser_open_is_true(self, browser_mocks: BrowserMocks):
83
+ options = await get_cli_run_options(
84
+ keep_browser_open=True,
85
+ headless=False,
86
+ )
87
+ assert options.environment == "cdp"
88
+ assert type(options) is CDPRunOptions
89
+ assert options.cdp_address == "http://localhost:1234"
90
+
91
+ from intuned_cli.utils.browser import _current_browser_context # type: ignore
92
+
93
+ first_context = _current_browser_context
94
+ assert first_context is not None
95
+
96
+ assert browser_mocks.launch_chromium.call_count == 1
97
+
98
+ options = await get_cli_run_options(
99
+ keep_browser_open=True,
100
+ headless=False,
101
+ )
102
+
103
+ from intuned_cli.utils.browser import _current_browser_context # type: ignore
104
+
105
+ second_context = _current_browser_context
106
+ assert second_context is not None
107
+ assert first_context != second_context
108
+ assert browser_mocks.launch_chromium.call_count == 2
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_keeps_context_open_until_explicitly_closed_if_keep_browser_open_is_true(
112
+ self, browser_mocks: BrowserMocks
113
+ ):
114
+ options = await get_cli_run_options(
115
+ keep_browser_open=True,
116
+ headless=False,
117
+ )
118
+ assert options.environment == "cdp"
119
+ assert type(options) is CDPRunOptions
120
+ assert options.cdp_address == "http://localhost:1234"
121
+
122
+ from intuned_cli.utils.browser import _current_browser_context # type: ignore
123
+
124
+ assert _current_browser_context is not None
125
+
126
+ assert browser_mocks.launch_chromium.call_count == 1
127
+
128
+ await close_cli_browser()
129
+
130
+ from intuned_cli.utils.browser import _current_browser_context # type: ignore
131
+
132
+ assert _current_browser_context is None
@@ -0,0 +1,27 @@
1
+ from unittest.mock import Mock
2
+ from unittest.mock import patch
3
+
4
+ from intuned_cli.utils.traces import cli_trace
5
+ from runtime.types.run_types import TracingDisabled
6
+ from runtime.types.run_types import TracingEnabled
7
+
8
+
9
+ def get_mock_console():
10
+ """Create a mock console that tracks calls."""
11
+ mock_console = Mock()
12
+ mock_console.print = Mock()
13
+ return mock_console
14
+
15
+
16
+ @patch("intuned_cli.controller.authsession.console", get_mock_console())
17
+ class TestTraces:
18
+ def test_gives_disabled_tracing_if_id_is_none(self):
19
+ tracing = cli_trace(None).__enter__()
20
+ assert tracing.enabled is False
21
+ assert type(tracing) is TracingDisabled
22
+
23
+ def test_gives_enabled_tracing_if_id_is_provided(self):
24
+ tracing = cli_trace("some_id").__enter__()
25
+ assert tracing.enabled is True
26
+ assert type(tracing) is TracingEnabled
27
+ assert "some_id" in tracing.file_path
@@ -1,7 +1,15 @@
1
+ import json
2
+ from typing import Any
3
+ from typing import Callable
4
+ from typing import get_args
1
5
  from typing import Literal
2
6
  from typing import overload
3
7
 
8
+ import toml
9
+ import yaml
4
10
  from anyio import Path
11
+ from jsonc_parser.parser import JsoncParser # type: ignore
12
+ from pydantic import BaseModel
5
13
 
6
14
  from intuned_cli.utils.error import CLIError
7
15
  from runtime.types import IntunedJson
@@ -23,10 +31,51 @@ async def assert_api_file_exists(dirname: Literal["api", "auth-sessions"], api_n
23
31
 
24
32
 
25
33
  async def load_intuned_json() -> IntunedJson:
26
- intuned_json_path = Path("Intuned.json")
27
- if not await intuned_json_path.exists():
28
- raise CLIError("Intuned.json file is missing in the current directory.")
34
+ intuned_settings_file = await get_intuned_settings_file()
35
+
36
+ intuned_settings_content = await Path(intuned_settings_file.file_path).read_text()
37
+
38
+ parsed_content = intuned_settings_file.parse(intuned_settings_content)
39
+
29
40
  try:
30
- return IntunedJson.model_validate_json(await intuned_json_path.read_text())
41
+ return IntunedJson.model_validate(parsed_content)
31
42
  except Exception as e:
32
- raise CLIError(f"Failed to parse Intuned.json: {e}") from e
43
+ raise CLIError(f"Failed to parse {intuned_settings_file.name}: {e}") from e
44
+
45
+
46
+ IntunedSettingsFileName = Literal["Intuned.json", "Intuned.jsonc", "Intuned.yaml", "Intuned.yml", "Intuned.toml"]
47
+
48
+ intuned_file_names: list[IntunedSettingsFileName] = list(get_args(IntunedSettingsFileName))
49
+
50
+
51
+ class IntunedSettingsFile(BaseModel):
52
+ name: IntunedSettingsFileName
53
+ file_path: str
54
+ parse: Callable[[str], Any]
55
+
56
+
57
+ intuned_settings_parsers: dict[IntunedSettingsFileName, Callable[[str], Any]] = {
58
+ "Intuned.json": json.loads,
59
+ "Intuned.jsonc": lambda content: JsoncParser.parse_str(content), # type: ignore
60
+ "Intuned.yaml": yaml.safe_load,
61
+ "Intuned.yml": yaml.safe_load,
62
+ "Intuned.toml": toml.loads,
63
+ }
64
+
65
+
66
+ async def get_intuned_settings_file() -> IntunedSettingsFile:
67
+ for file_name in intuned_file_names:
68
+ path = Path(file_name)
69
+ if await path.exists():
70
+ return IntunedSettingsFile(
71
+ name=file_name,
72
+ file_path=str(path),
73
+ parse=intuned_settings_parsers[file_name],
74
+ )
75
+ raise CLIError(
76
+ "No Intuned settings file found in the current directory. Expected one of: " + ", ".join(intuned_file_names)
77
+ )
78
+
79
+
80
+ async def get_intuned_settings_file_name() -> str:
81
+ return (await get_intuned_settings_file()).name
@@ -1,10 +1,13 @@
1
1
  import json
2
2
  from typing import Any
3
+ from typing import Literal
3
4
 
4
5
  from anyio import Path
5
6
 
7
+ from intuned_cli.utils.api_helpers import get_intuned_settings_file_name
6
8
  from intuned_cli.utils.api_helpers import load_intuned_json
7
9
  from intuned_cli.utils.error import CLIError
10
+ from runtime.types.settings_types import IntunedJsonEnabledAuthSessions
8
11
 
9
12
 
10
13
  class CLIAssertionError(CLIError):
@@ -12,17 +15,30 @@ class CLIAssertionError(CLIError):
12
15
 
13
16
 
14
17
  async def is_auth_enabled() -> bool:
15
- """
16
- Check if the auth session is enabled in Intuned.json.
17
- Returns True if enabled, False otherwise.
18
- """
19
18
  intuned_json = await load_intuned_json()
20
19
  return intuned_json.auth_sessions.enabled
21
20
 
22
21
 
23
- async def assert_auth_enabled():
22
+ async def assert_auth_enabled(*, auth_type: Literal["API", "MANUAL"] | None = None):
24
23
  if not await is_auth_enabled():
25
- raise CLIAssertionError("Auth session is not enabled, enable it in Intuned.json to use it")
24
+ raise CLIAssertionError(
25
+ f"Auth session is not enabled, enable it in {await get_intuned_settings_file_name()} to use it"
26
+ )
27
+ if auth_type is None:
28
+ return
29
+ intuned_json = await load_intuned_json()
30
+ if (
31
+ type(intuned_json.auth_sessions) is IntunedJsonEnabledAuthSessions
32
+ and intuned_json.auth_sessions.type != auth_type
33
+ ):
34
+ if auth_type == "API":
35
+ raise CLIAssertionError(
36
+ f"Auth session type is not credentials-based in {await get_intuned_settings_file_name()}. Set it to 'API' to run this command."
37
+ )
38
+
39
+ raise CLIAssertionError(
40
+ f"Auth session type is not recorder-based in {await get_intuned_settings_file_name()}. Set it to 'MANUAL' to run this command."
41
+ )
26
42
 
27
43
 
28
44
  async def assert_auth_consistent(auth_session_id: str | None = None):
@@ -32,7 +48,9 @@ async def assert_auth_consistent(auth_session_id: str | None = None):
32
48
  "Auth session is enabled, but no auth session is provided. Please provide an auth session ID."
33
49
  )
34
50
  if not _is_auth_enabled and auth_session_id is not None:
35
- raise CLIAssertionError("Auth session is not enabled, enable it in Intuned.json to use it")
51
+ raise CLIAssertionError(
52
+ f"Auth session is not enabled, enable it in {await get_intuned_settings_file_name()} to use it"
53
+ )
36
54
 
37
55
 
38
56
  async def load_parameters(parameters: str) -> dict[str, Any]:
@@ -55,3 +73,20 @@ async def load_parameters(parameters: str) -> dict[str, Any]:
55
73
  raise CLIError(f"Invalid JSON format: {e}") from e
56
74
  except Exception as e:
57
75
  raise CLIError(f"Failed to load parameters: {e}") from e
76
+
77
+
78
+ async def get_auth_session_recorder_parameters() -> tuple[str, str]:
79
+ intuned_json = await load_intuned_json()
80
+ auth_session_settings = intuned_json.auth_sessions
81
+ if type(auth_session_settings) is not IntunedJsonEnabledAuthSessions:
82
+ raise CLIError(f"Auth sessions are not enabled in {await get_intuned_settings_file_name()}")
83
+ if auth_session_settings.type != "MANUAL":
84
+ raise CLIError(f"Auth session type is not recorder-based in {await get_intuned_settings_file_name()}")
85
+ start_url = auth_session_settings.start_url
86
+ finish_url = auth_session_settings.finish_url
87
+ if not start_url or not finish_url:
88
+ raise CLIError(
89
+ f"Auth session type is recorder-based but start_url or finish_url is not set in {await get_intuned_settings_file_name()}"
90
+ )
91
+
92
+ return start_url, finish_url
@@ -1,5 +1,6 @@
1
1
  import os
2
2
 
3
+ from intuned_cli.utils.api_helpers import get_intuned_settings_file_name
3
4
  from intuned_cli.utils.error import CLIError
4
5
  from runtime.types import IntunedJson
5
6
 
@@ -21,7 +22,9 @@ async def get_intuned_api_auth_credentials(
21
22
  api_key = api_key or os.environ.get("INTUNED_API_KEY")
22
23
 
23
24
  if not workspace_id:
24
- raise CLIError("Workspace ID is required. Please provide it via command line options or Intuned.json")
25
+ raise CLIError(
26
+ f"Workspace ID is required. Please provide it via command line options or {await get_intuned_settings_file_name()}."
27
+ )
25
28
 
26
29
  if not api_key:
27
30
  raise CLIError(
@@ -0,0 +1,63 @@
1
+ from typing import Any
2
+ from typing import AsyncContextManager
3
+ from typing import TYPE_CHECKING
4
+
5
+ from runtime.types.run_types import CDPRunOptions
6
+ from runtime.types.run_types import ProxyConfig
7
+ from runtime.types.run_types import StandaloneRunOptions
8
+
9
+ if TYPE_CHECKING:
10
+ from playwright.async_api import BrowserContext
11
+
12
+ _current_browser_context_manager: AsyncContextManager[Any] | None = None
13
+ _current_browser_context: "BrowserContext | None" = None
14
+
15
+
16
+ async def get_cli_run_options(
17
+ headless: bool = False,
18
+ proxy: ProxyConfig | None = None,
19
+ keep_browser_open: bool = False,
20
+ ):
21
+ global _current_browser_context_manager, _current_browser_context
22
+ if not keep_browser_open:
23
+ return StandaloneRunOptions(
24
+ headless=headless,
25
+ proxy=proxy,
26
+ )
27
+ from playwright.async_api import ProxySettings
28
+
29
+ from runtime.browser.launch_chromium import launch_chromium
30
+ from runtime.run.playwright_context import get_random_free_port
31
+
32
+ await close_cli_browser()
33
+ port = await get_random_free_port()
34
+ acm = launch_chromium(
35
+ headless=headless,
36
+ cdp_port=port,
37
+ proxy=ProxySettings(
38
+ **proxy.model_dump(by_alias=True),
39
+ )
40
+ if proxy
41
+ else None,
42
+ )
43
+
44
+ _current_browser_context_manager = acm
45
+ _current_browser_context, _ = await _current_browser_context_manager.__aenter__()
46
+
47
+ return CDPRunOptions(
48
+ cdp_address=f"http://localhost:{port}",
49
+ )
50
+
51
+
52
+ def is_cli_browser_launched():
53
+ global _current_browser_context_manager
54
+ return _current_browser_context_manager is not None and _current_browser_context is not None
55
+
56
+
57
+ async def close_cli_browser():
58
+ global _current_browser_context_manager, _current_browser_context
59
+ if _current_browser_context_manager is not None:
60
+ await _current_browser_context_manager.__aexit__(None, None, None)
61
+ _current_browser_context_manager = None
62
+ if _current_browser_context is not None:
63
+ _current_browser_context = None
@@ -29,3 +29,17 @@ def log_automation_error(e: RunApiError):
29
29
  console.print(f"[red]{''.join(stack_trace)}[/red]")
30
30
  else:
31
31
  console.print(f"[red]{e}[/red]")
32
+
33
+
34
+ class CLIExit(BaseException):
35
+ """Exception to signal CLI exit with a specific code."""
36
+
37
+ def __init__(self, code: int):
38
+ """
39
+ Initialize the CLIExit.
40
+
41
+ Args:
42
+ code (int): The exit code.
43
+ """
44
+ super().__init__()
45
+ self.code = code
@@ -34,6 +34,7 @@ exclusions = [
34
34
  "README.md",
35
35
  "output/**",
36
36
  "tmp/**",
37
+ "traces/**",
37
38
  ]
38
39
 
39
40
  # For compatibility with import style similar to 'export default exclusions'
@@ -0,0 +1,9 @@
1
+ import arguably
2
+
3
+ from intuned_cli.utils.error import CLIExit
4
+
5
+
6
+ def print_help_and_exit():
7
+ if arguably.is_target():
8
+ arguably._context.context._current_parser.print_help() # type: ignore
9
+ raise CLIExit(0)