intuned-runtime 1.3.1__py3-none-any.whl → 1.3.2__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 +15 -24
- intuned_cli/commands/__init__.py +6 -1
- intuned_cli/commands/attempt_api_command.py +8 -0
- intuned_cli/commands/attempt_authsession_check_command.py +8 -0
- intuned_cli/commands/attempt_authsession_command.py +4 -4
- intuned_cli/commands/attempt_authsession_create_command.py +9 -1
- intuned_cli/commands/attempt_command.py +4 -4
- intuned_cli/commands/authsession_command.py +12 -0
- intuned_cli/commands/authsession_record_command.py +54 -0
- intuned_cli/commands/command.py +6 -4
- intuned_cli/commands/deploy_command.py +2 -0
- intuned_cli/commands/init_command.py +2 -0
- intuned_cli/commands/run_api_command.py +9 -1
- intuned_cli/commands/run_authsession_command.py +4 -4
- intuned_cli/commands/run_authsession_create_command.py +34 -4
- intuned_cli/commands/run_authsession_update_command.py +33 -4
- intuned_cli/commands/run_authsession_validate_command.py +32 -3
- intuned_cli/commands/run_command.py +4 -4
- intuned_cli/commands/save_command.py +2 -0
- intuned_cli/controller/__test__/test_api.py +159 -18
- intuned_cli/controller/__test__/test_authsession.py +497 -6
- intuned_cli/controller/api.py +40 -39
- intuned_cli/controller/authsession.py +213 -110
- intuned_cli/controller/deploy.py +3 -3
- intuned_cli/controller/save.py +47 -48
- intuned_cli/types.py +14 -0
- intuned_cli/utils/__test__/test_browser.py +132 -0
- intuned_cli/utils/__test__/test_traces.py +27 -0
- intuned_cli/utils/api_helpers.py +54 -5
- intuned_cli/utils/auth_session_helpers.py +42 -7
- intuned_cli/utils/backend.py +4 -1
- intuned_cli/utils/browser.py +63 -0
- intuned_cli/utils/error.py +14 -0
- intuned_cli/utils/exclusions.py +1 -0
- intuned_cli/utils/help.py +9 -0
- intuned_cli/utils/traces.py +31 -0
- intuned_cli/utils/wrapper.py +58 -0
- intuned_internal_cli/__init__.py +7 -0
- {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.2.dist-info}/METADATA +4 -2
- {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.2.dist-info}/RECORD +45 -37
- runtime/browser/launch_chromium.py +4 -0
- runtime/types/settings_types.py +13 -4
- {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.2.dist-info}/WHEEL +0 -0
- {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.2.dist-info}/entry_points.txt +0 -0
- {intuned_runtime-1.3.1.dist-info → intuned_runtime-1.3.2.dist-info}/licenses/LICENSE +0 -0
intuned_cli/controller/deploy.py
CHANGED
@@ -30,7 +30,7 @@ async def check_deploy_status(
|
|
30
30
|
api_key: str,
|
31
31
|
):
|
32
32
|
base_url = get_base_url()
|
33
|
-
url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/
|
33
|
+
url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/{project_name}/deploy/result"
|
34
34
|
|
35
35
|
headers = {
|
36
36
|
"x-api-key": api_key,
|
@@ -124,7 +124,7 @@ async def deploy_project(
|
|
124
124
|
if update_console_task:
|
125
125
|
update_console_task.cancel()
|
126
126
|
if console.is_terminal:
|
127
|
-
print("\r", " " * 100)
|
127
|
+
print("\r", " " * 100, file=console.file)
|
128
128
|
console.print("[green][bold]Project deployed successfully![/bold][/green]")
|
129
129
|
console.print(
|
130
130
|
f"[bold]You can check your project on the platform:[/bold] [cyan underline]{get_base_url()}/projects/{project_name}/details[/cyan underline]"
|
@@ -143,7 +143,7 @@ async def deploy_project(
|
|
143
143
|
)
|
144
144
|
except Exception:
|
145
145
|
if console.is_terminal:
|
146
|
-
print("\r", " " * 100)
|
146
|
+
print("\r", " " * 100, file=console.file)
|
147
147
|
raise
|
148
148
|
finally:
|
149
149
|
if update_console_task:
|
intuned_cli/controller/save.py
CHANGED
@@ -16,6 +16,7 @@ from intuned_cli.types import DirectoryNode
|
|
16
16
|
from intuned_cli.types import FileNode
|
17
17
|
from intuned_cli.types import FileNodeContent
|
18
18
|
from intuned_cli.types import FileSystemTree
|
19
|
+
from intuned_cli.utils.api_helpers import get_intuned_settings_file
|
19
20
|
from intuned_cli.utils.api_helpers import load_intuned_json
|
20
21
|
from intuned_cli.utils.backend import get_base_url
|
21
22
|
from intuned_cli.utils.console import console
|
@@ -123,70 +124,69 @@ async def get_file_tree_from_project(path: Path, *, exclude: list[str] | None =
|
|
123
124
|
return results
|
124
125
|
|
125
126
|
|
126
|
-
def
|
127
|
+
async def map_file_tree_to_ide_file_tree(file_tree: FileSystemTree):
|
127
128
|
"""
|
128
129
|
Maps the file tree to IDE parameters format by processing parameters directory
|
129
130
|
and converting it to ____testParameters structure.
|
130
131
|
"""
|
131
132
|
|
132
|
-
if not file_tree:
|
133
|
-
return
|
134
|
-
|
135
133
|
parameters_node = file_tree.root.get("parameters")
|
136
|
-
if parameters_node
|
137
|
-
|
134
|
+
if isinstance(parameters_node, DirectoryNode):
|
135
|
+
api_parameters_map: dict[str, list[dict[str, Any]]] = {}
|
136
|
+
cli_parameters = list(parameters_node.directory.root.keys())
|
137
|
+
test_parameters = DirectoryNode(directory=FileSystemTree(root={}))
|
138
138
|
|
139
|
-
|
140
|
-
|
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={}))
|
139
|
+
for parameter_key in cli_parameters:
|
140
|
+
# If parameter of type directory, discard it and continue
|
141
|
+
parameter = parameters_node.directory.root[parameter_key]
|
145
142
|
|
146
|
-
|
147
|
-
|
148
|
-
parameter = parameters_node.directory.root[parameter_key]
|
143
|
+
if isinstance(parameter, DirectoryNode):
|
144
|
+
continue
|
149
145
|
|
150
|
-
|
151
|
-
|
146
|
+
if not parameter.file.contents.strip():
|
147
|
+
continue
|
152
148
|
|
153
|
-
|
154
|
-
|
149
|
+
try:
|
150
|
+
parameter_payload = json.loads(parameter.file.contents)
|
151
|
+
except json.JSONDecodeError:
|
152
|
+
continue
|
155
153
|
|
156
|
-
|
157
|
-
|
158
|
-
except json.JSONDecodeError:
|
159
|
-
continue
|
154
|
+
if "__api-name" not in parameter_payload:
|
155
|
+
continue
|
160
156
|
|
161
|
-
|
162
|
-
|
157
|
+
api = parameter_payload["__api-name"]
|
158
|
+
# Create parameter value by excluding __api-name
|
159
|
+
parameter_value = {k: v for k, v in parameter_payload.items() if k != "__api-name"}
|
163
160
|
|
164
|
-
|
165
|
-
|
166
|
-
|
161
|
+
test_parameter: dict[str, Any] = {
|
162
|
+
"name": parameter_key.replace(".json", ""),
|
163
|
+
"lastUsed": False,
|
164
|
+
"id": str(uuid.uuid4()),
|
165
|
+
"value": json.dumps(parameter_value),
|
166
|
+
}
|
167
167
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
"id": str(uuid.uuid4()),
|
172
|
-
"value": json.dumps(parameter_value),
|
173
|
-
}
|
168
|
+
if api not in api_parameters_map:
|
169
|
+
api_parameters_map[api] = []
|
170
|
+
api_parameters_map[api].append(test_parameter)
|
174
171
|
|
175
|
-
|
176
|
-
|
177
|
-
|
172
|
+
for api, parameters in api_parameters_map.items():
|
173
|
+
# By default, last one used is the last one in the map
|
174
|
+
if len(parameters) > 0:
|
175
|
+
parameters[-1]["lastUsed"] = True
|
178
176
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
parameters[-1]["lastUsed"] = True
|
177
|
+
test_parameters.directory.root[f"{api}.json"] = FileNode(
|
178
|
+
file=FileNodeContent(contents=json.dumps(parameters, indent=2))
|
179
|
+
)
|
183
180
|
|
184
|
-
|
185
|
-
|
186
|
-
)
|
181
|
+
del file_tree.root["parameters"]
|
182
|
+
file_tree.root["____testParameters"] = test_parameters
|
187
183
|
|
188
|
-
|
189
|
-
|
184
|
+
if file_tree.root.get("Intuned.json") is None:
|
185
|
+
settings_file = await get_intuned_settings_file()
|
186
|
+
text_content = await Path(settings_file.file_path).read_text()
|
187
|
+
parsed_content = settings_file.parse(text_content)
|
188
|
+
json_content = json.dumps(parsed_content, indent=2)
|
189
|
+
file_tree.root["Intuned.json"] = FileNode(file=FileNodeContent(contents=json_content))
|
190
190
|
|
191
191
|
|
192
192
|
class SaveProjectResponse(BaseModel):
|
@@ -201,14 +201,13 @@ async def save_project(
|
|
201
201
|
):
|
202
202
|
base_url = get_base_url()
|
203
203
|
url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/{project_name}"
|
204
|
-
print(f"calling {url}")
|
205
204
|
headers = {
|
206
205
|
api_key_header_name: api_key,
|
207
206
|
"Content-Type": "application/json",
|
208
207
|
}
|
209
208
|
cwd = await Path().resolve()
|
210
209
|
file_tree = await get_file_tree_from_project(cwd, exclude=exclusions)
|
211
|
-
|
210
|
+
await map_file_tree_to_ide_file_tree(file_tree)
|
212
211
|
|
213
212
|
payload: dict[str, Any] = {
|
214
213
|
"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
|
intuned_cli/utils/api_helpers.py
CHANGED
@@ -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
|
-
|
27
|
-
|
28
|
-
|
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.
|
41
|
+
return IntunedJson.model_validate(parsed_content)
|
31
42
|
except Exception as e:
|
32
|
-
raise CLIError(f"Failed to parse
|
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(
|
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(
|
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
|
intuned_cli/utils/backend.py
CHANGED
@@ -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(
|
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
|
intuned_cli/utils/error.py
CHANGED
@@ -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
|
intuned_cli/utils/exclusions.py
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
from contextlib import contextmanager
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from intuned_cli.utils.console import console
|
6
|
+
from runtime.types.run_types import TracingDisabled
|
7
|
+
from runtime.types.run_types import TracingEnabled
|
8
|
+
|
9
|
+
_trace_dir_name = datetime.now().isoformat()
|
10
|
+
|
11
|
+
_count = 0
|
12
|
+
|
13
|
+
|
14
|
+
def get_trace_path(id: str):
|
15
|
+
global _count
|
16
|
+
_count += 1
|
17
|
+
return Path() / "traces" / _trace_dir_name / f"{_count}-{id}.zip"
|
18
|
+
|
19
|
+
|
20
|
+
@contextmanager
|
21
|
+
def cli_trace(id: str | None):
|
22
|
+
if not id:
|
23
|
+
yield TracingDisabled()
|
24
|
+
return
|
25
|
+
|
26
|
+
trace_path = get_trace_path(id)
|
27
|
+
try:
|
28
|
+
yield TracingEnabled(file_path=str(trace_path))
|
29
|
+
finally:
|
30
|
+
if trace_path.exists():
|
31
|
+
console.print(f"[bold]Trace saved to [/bold][underline]{str(trace_path)}[/underline]")
|