intuned-runtime 1.0.0__py3-none-any.whl → 1.1.0__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 +40 -0
- intuned_cli/commands/__init__.py +18 -0
- intuned_cli/commands/attempt_api_command.py +51 -0
- intuned_cli/commands/attempt_authsession_check_command.py +38 -0
- intuned_cli/commands/attempt_authsession_command.py +12 -0
- intuned_cli/commands/attempt_authsession_create_command.py +44 -0
- intuned_cli/commands/attempt_command.py +12 -0
- intuned_cli/commands/command.py +26 -0
- intuned_cli/commands/deploy_command.py +47 -0
- intuned_cli/commands/init_command.py +21 -0
- intuned_cli/commands/run_api_command.py +69 -0
- intuned_cli/commands/run_authsession_command.py +12 -0
- intuned_cli/commands/run_authsession_create_command.py +50 -0
- intuned_cli/commands/run_authsession_update_command.py +52 -0
- intuned_cli/commands/run_authsession_validate_command.py +49 -0
- intuned_cli/commands/run_command.py +12 -0
- intuned_cli/constants/__init__.py +1 -0
- intuned_cli/constants/readme.py +134 -0
- intuned_cli/controller/__test__/__init__.py +0 -0
- intuned_cli/controller/__test__/test_api.py +529 -0
- intuned_cli/controller/__test__/test_authsession.py +907 -0
- intuned_cli/controller/api.py +212 -0
- intuned_cli/controller/authsession.py +458 -0
- intuned_cli/controller/deploy.py +352 -0
- intuned_cli/controller/init.py +97 -0
- intuned_cli/types.py +33 -0
- intuned_cli/utils/api_helpers.py +32 -0
- intuned_cli/utils/auth_session_helpers.py +57 -0
- intuned_cli/utils/backend.py +5 -0
- intuned_cli/utils/confirmation.py +0 -0
- intuned_cli/utils/console.py +6 -0
- intuned_cli/utils/error.py +27 -0
- intuned_cli/utils/exclusions.py +40 -0
- intuned_cli/utils/get_auth_parameters.py +18 -0
- intuned_cli/utils/import_function.py +15 -0
- intuned_cli/utils/timeout.py +25 -0
- {cli → intuned_internal_cli}/__init__.py +1 -1
- {cli → intuned_internal_cli}/commands/__init__.py +2 -0
- {cli → intuned_internal_cli}/commands/ai_source/deploy.py +1 -1
- {cli → intuned_internal_cli}/commands/project/type_check.py +39 -32
- intuned_internal_cli/commands/root.py +15 -0
- {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.0.dist-info}/METADATA +3 -1
- intuned_runtime-1.1.0.dist-info/RECORD +96 -0
- intuned_runtime-1.1.0.dist-info/entry_points.txt +4 -0
- runtime/__init__.py +2 -1
- runtime/backend_functions/_call_backend_function.py +0 -5
- runtime/browser/__init__.py +2 -1
- runtime/browser/launch_chromium.py +68 -49
- runtime/browser/storage_state.py +11 -12
- runtime/errors/run_api_errors.py +14 -10
- runtime/run/playwright_constructs.py +4 -2
- runtime/run/pydantic_encoder.py +15 -0
- runtime/run/run_api.py +5 -4
- runtime/types/run_types.py +16 -0
- intuned_runtime-1.0.0.dist-info/RECORD +0 -58
- intuned_runtime-1.0.0.dist-info/entry_points.txt +0 -3
- {cli → intuned_internal_cli}/commands/ai_source/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/ai_source/ai_source.py +0 -0
- {cli → intuned_internal_cli}/commands/browser/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/browser/save_state.py +0 -0
- {cli → intuned_internal_cli}/commands/init.py +0 -0
- {cli → intuned_internal_cli}/commands/project/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/check.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/create.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/load.py +0 -0
- {cli → intuned_internal_cli}/commands/project/project.py +0 -0
- {cli → intuned_internal_cli}/commands/project/run.py +0 -0
- {cli → intuned_internal_cli}/commands/project/run_interface.py +0 -0
- {cli → intuned_internal_cli}/commands/project/upgrade.py +0 -0
- {cli → intuned_internal_cli}/commands/publish_packages.py +0 -0
- {cli → intuned_internal_cli}/logger.py +0 -0
- {cli → intuned_internal_cli}/utils/ai_source_project.py +0 -0
- {cli → intuned_internal_cli}/utils/code_tree.py +0 -0
- {cli → intuned_internal_cli}/utils/run_apis.py +0 -0
- {cli → intuned_internal_cli}/utils/unix_socket.py +0 -0
- {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.0.dist-info}/LICENSE +0 -0
- {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,352 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import time
|
6
|
+
import uuid
|
7
|
+
from itertools import cycle
|
8
|
+
from typing import Any
|
9
|
+
from typing import Literal
|
10
|
+
|
11
|
+
import httpx
|
12
|
+
import pathspec
|
13
|
+
import toml
|
14
|
+
from anyio import Path
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
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
|
23
|
+
from intuned_cli.utils.backend import get_base_url
|
24
|
+
from intuned_cli.utils.console import console
|
25
|
+
from intuned_cli.utils.error import CLIError
|
26
|
+
from intuned_cli.utils.exclusions import exclusions
|
27
|
+
|
28
|
+
supported_playwright_versions = ["1.46.0", "1.52.0"]
|
29
|
+
|
30
|
+
project_deploy_timeout = 10 * 60
|
31
|
+
project_deploy_check_period = 5
|
32
|
+
|
33
|
+
|
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
|
+
class DeployStatus(BaseModel):
|
218
|
+
status: Literal["completed", "failed", "pending"]
|
219
|
+
message: str | None = None
|
220
|
+
reason: str | None = None
|
221
|
+
|
222
|
+
|
223
|
+
async def check_deploy_status(
|
224
|
+
*,
|
225
|
+
project_name: str,
|
226
|
+
workspace_id: str,
|
227
|
+
api_key: str,
|
228
|
+
):
|
229
|
+
base_url = get_base_url()
|
230
|
+
url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/create/{project_name}/result"
|
231
|
+
|
232
|
+
headers = {
|
233
|
+
"x-api-key": api_key,
|
234
|
+
"Content-Type": "application/json",
|
235
|
+
}
|
236
|
+
async with httpx.AsyncClient() as client:
|
237
|
+
response = await client.get(url, headers=headers)
|
238
|
+
if response.status_code < 200 or response.status_code >= 300:
|
239
|
+
if response.status_code == 401:
|
240
|
+
raise CLIError("Invalid API key. Please check your API key and try again.")
|
241
|
+
if response.status_code == 404:
|
242
|
+
raise CLIError(f"Project '{project_name}' not found in workspace '{workspace_id}'.")
|
243
|
+
raise CLIError(f"Failed to check deploy status for project '{project_name}': {response.text}")
|
244
|
+
|
245
|
+
data = response.json()
|
246
|
+
try:
|
247
|
+
deploy_status = DeployStatus.model_validate(data)
|
248
|
+
except Exception as e:
|
249
|
+
raise CLIError(f"Failed to parse deploy status response: {e}") from e
|
250
|
+
|
251
|
+
return deploy_status
|
252
|
+
|
253
|
+
|
254
|
+
async def deploy_project(
|
255
|
+
*,
|
256
|
+
project_name: str,
|
257
|
+
workspace_id: str,
|
258
|
+
api_key: str,
|
259
|
+
):
|
260
|
+
base_url = get_base_url()
|
261
|
+
url = f"{base_url}/api/v1/workspace/{workspace_id}/projects/create"
|
262
|
+
headers = {
|
263
|
+
"x-api-key": api_key,
|
264
|
+
"Content-Type": "application/json",
|
265
|
+
}
|
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
|
+
|
277
|
+
async with httpx.AsyncClient() as client:
|
278
|
+
response = await client.post(url, headers=headers, json=payload)
|
279
|
+
if response.status_code < 200 or response.status_code >= 300:
|
280
|
+
if response.status_code == 401:
|
281
|
+
raise CLIError("Invalid API key. Please check your API key and try again.")
|
282
|
+
|
283
|
+
raise CLIError(
|
284
|
+
f"[red bold]Invalid response from server:[/red bold]\n [bright_red]{response.status_code} {response.text}[/bright_red][red bold]\nProject deployment failed.[/red bold]"
|
285
|
+
)
|
286
|
+
|
287
|
+
start_time = time.time()
|
288
|
+
|
289
|
+
async def update_console():
|
290
|
+
for spinner in cycle("⠙⠹⠸⠼⠴⠦⠧⠇"):
|
291
|
+
await asyncio.sleep(0.05)
|
292
|
+
|
293
|
+
time_elapsed_text = f"{time.time() - start_time:.1f}"
|
294
|
+
print("\r", end="", flush=True)
|
295
|
+
console.print(
|
296
|
+
f"{spinner} [cyan]Deploying[/cyan] [bright_black]({time_elapsed_text}s)[/bright_black] ", end=""
|
297
|
+
)
|
298
|
+
|
299
|
+
if console.is_terminal:
|
300
|
+
update_console_task = asyncio.create_task(update_console())
|
301
|
+
else:
|
302
|
+
update_console_task = None
|
303
|
+
console.print("[cyan]Deploying[/cyan]")
|
304
|
+
|
305
|
+
try:
|
306
|
+
while True:
|
307
|
+
await asyncio.sleep(project_deploy_check_period)
|
308
|
+
if not console.is_terminal:
|
309
|
+
time_elapsed_text = f"{time.time() - start_time:.1f}"
|
310
|
+
console.print(f"[cyan]Deploying[/cyan] [bright_black]({time_elapsed_text}s)[/bright_black]")
|
311
|
+
|
312
|
+
try:
|
313
|
+
deploy_status = await check_deploy_status(
|
314
|
+
project_name=project_name,
|
315
|
+
workspace_id=workspace_id,
|
316
|
+
api_key=api_key,
|
317
|
+
)
|
318
|
+
|
319
|
+
if deploy_status.status == "pending":
|
320
|
+
elapsed_time = time.time() - start_time
|
321
|
+
if elapsed_time > project_deploy_timeout:
|
322
|
+
raise CLIError(f"Deployment timed out after {project_deploy_timeout//60} minutes.")
|
323
|
+
continue
|
324
|
+
|
325
|
+
if deploy_status.status == "completed":
|
326
|
+
if update_console_task:
|
327
|
+
update_console_task.cancel()
|
328
|
+
if console.is_terminal:
|
329
|
+
print("\r", " " * 100)
|
330
|
+
console.print("[green][bold]Project deployed successfully![/bold][/green]")
|
331
|
+
console.print(
|
332
|
+
f"[bold]You can check your project on the platform:[/bold] [cyan underline]{get_base_url()}/projects/{project_name}/details[/cyan underline]"
|
333
|
+
)
|
334
|
+
return
|
335
|
+
|
336
|
+
error_message = (
|
337
|
+
f"[red bold]Project deployment failed:[/bold red]\n{deploy_status.message or 'Unknown error'}\n"
|
338
|
+
)
|
339
|
+
if deploy_status.reason:
|
340
|
+
error_message += f"Reason: {deploy_status.reason}\n"
|
341
|
+
error_message += "[red bold]Project deployment failed[/red bold]"
|
342
|
+
raise CLIError(
|
343
|
+
error_message,
|
344
|
+
auto_color=False,
|
345
|
+
)
|
346
|
+
except Exception:
|
347
|
+
if console.is_terminal:
|
348
|
+
print("\r", " " * 100)
|
349
|
+
raise
|
350
|
+
finally:
|
351
|
+
if update_console_task:
|
352
|
+
update_console_task.cancel()
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
from anyio import Path
|
5
|
+
|
6
|
+
from intuned_cli.constants import readme
|
7
|
+
from intuned_cli.types import DirectoryNode
|
8
|
+
from intuned_cli.types import FileNode
|
9
|
+
from intuned_cli.types import FileNodeContent
|
10
|
+
from intuned_cli.types import FileSystemTree
|
11
|
+
from intuned_cli.utils.backend import get_base_url
|
12
|
+
from intuned_cli.utils.console import console
|
13
|
+
from intuned_cli.utils.error import CLIError
|
14
|
+
from intuned_cli.utils.exclusions import exclusions as default_excludes
|
15
|
+
|
16
|
+
PythonTemplateName = Literal["python-empty"]
|
17
|
+
|
18
|
+
python_template_name: PythonTemplateName = "python-empty"
|
19
|
+
|
20
|
+
|
21
|
+
async def check_empty_directory() -> bool:
|
22
|
+
cwd = await Path().resolve()
|
23
|
+
try:
|
24
|
+
if not await cwd.is_dir():
|
25
|
+
raise CLIError("The current path is not a directory.")
|
26
|
+
|
27
|
+
files = [f async for f in cwd.iterdir() if await f.is_file()]
|
28
|
+
significant_files = [f for f in files if f.name not in default_excludes]
|
29
|
+
|
30
|
+
return len(significant_files) == 0
|
31
|
+
except FileNotFoundError as e:
|
32
|
+
raise CLIError("The specified directory does not exist.") from e
|
33
|
+
|
34
|
+
|
35
|
+
async def fetch_project_template(template_name: PythonTemplateName) -> FileSystemTree:
|
36
|
+
"""
|
37
|
+
Fetch the project template from the templates directory.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
template_name (PythonTemplateName): The name of the template to fetch.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
FileSystemTree: The fetched template.
|
44
|
+
"""
|
45
|
+
base_url = get_base_url()
|
46
|
+
url = f"{base_url}/api/templates/{template_name}"
|
47
|
+
async with httpx.AsyncClient() as client:
|
48
|
+
response = await client.get(url)
|
49
|
+
if response.status_code != 200:
|
50
|
+
raise CLIError(f"Failed to fetch template '{template_name}': {response.text}")
|
51
|
+
template_data = response.json()
|
52
|
+
return FileSystemTree.model_validate(template_data["template"])
|
53
|
+
|
54
|
+
|
55
|
+
def prepare_cli_template(file_tree: FileSystemTree):
|
56
|
+
file_tree.root["parameters"] = DirectoryNode(directory=FileSystemTree(root={}))
|
57
|
+
|
58
|
+
file_tree.root["README.md"] = FileNode(file=FileNodeContent(contents=readme))
|
59
|
+
|
60
|
+
|
61
|
+
async def mount_file_tree(file_tree: FileSystemTree, working_directory: Path | None = None):
|
62
|
+
working_directory = working_directory or await Path().resolve()
|
63
|
+
if not await working_directory.is_dir():
|
64
|
+
raise CLIError(f"The specified working directory '{working_directory}' is not a directory.")
|
65
|
+
for name, node in file_tree.root.items():
|
66
|
+
node_path = working_directory / name
|
67
|
+
if isinstance(node, DirectoryNode):
|
68
|
+
await node_path.mkdir(parents=True, exist_ok=True)
|
69
|
+
await mount_file_tree(node.directory, working_directory=node_path)
|
70
|
+
else:
|
71
|
+
await node_path.write_text(node.file.contents)
|
72
|
+
|
73
|
+
|
74
|
+
async def write_project_from_file_tree(template_name: PythonTemplateName, is_target_directory_empty: bool):
|
75
|
+
if not is_target_directory_empty:
|
76
|
+
response = (
|
77
|
+
console.input(
|
78
|
+
"[bold]The current directory is not empty. Do you want to proceed and override files?[/bold] (y/N)"
|
79
|
+
)
|
80
|
+
.strip()
|
81
|
+
.lower()
|
82
|
+
)
|
83
|
+
confirmed = response in ["y", "yes"]
|
84
|
+
if not confirmed:
|
85
|
+
raise CLIError("Project initialization cancelled")
|
86
|
+
|
87
|
+
console.print(f"[cyan]🚀 Initializing[/cyan] [bold]{template_name}[/bold] [cyan]project...[/cyan]")
|
88
|
+
|
89
|
+
project_template = await fetch_project_template(template_name)
|
90
|
+
console.print("[cyan]🔨 Creating project files...[/cyan]")
|
91
|
+
|
92
|
+
prepare_cli_template(project_template)
|
93
|
+
await mount_file_tree(project_template)
|
94
|
+
|
95
|
+
console.print(
|
96
|
+
"[green][bold]🎉 Project initialized successfully![/bold][/green] [bright_green]Run[/bright_green] [bold]poetry install[/bold] [bright_green]to install dependencies and start coding![/bright_green]"
|
97
|
+
)
|
intuned_cli/types.py
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
from pydantic import Field
|
3
|
+
from pydantic import RootModel
|
4
|
+
|
5
|
+
|
6
|
+
class FileSystemTree(RootModel[dict[str, "DirectoryNode | FileNode"]]):
|
7
|
+
root: dict[str, "DirectoryNode | FileNode"]
|
8
|
+
|
9
|
+
|
10
|
+
class DirectoryNode(BaseModel):
|
11
|
+
directory: "FileSystemTree"
|
12
|
+
|
13
|
+
|
14
|
+
class FileNodeContent(BaseModel):
|
15
|
+
contents: str
|
16
|
+
|
17
|
+
|
18
|
+
class FileNode(BaseModel):
|
19
|
+
file: "FileNodeContent"
|
20
|
+
|
21
|
+
|
22
|
+
FileSystemTree.model_rebuild()
|
23
|
+
|
24
|
+
|
25
|
+
class IntunedJson(BaseModel):
|
26
|
+
model_config = {"populate_by_name": True}
|
27
|
+
|
28
|
+
class _AuthSessions(BaseModel):
|
29
|
+
enabled: bool
|
30
|
+
|
31
|
+
auth_sessions: _AuthSessions = Field(alias="authSessions")
|
32
|
+
project_name: str | None = Field(alias="projectName", default=None)
|
33
|
+
workspace_id: str | None = Field(alias="workspaceId", default=None)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
from typing import overload
|
3
|
+
|
4
|
+
from anyio import Path
|
5
|
+
|
6
|
+
from intuned_cli.types import IntunedJson
|
7
|
+
from intuned_cli.utils.error import CLIError
|
8
|
+
|
9
|
+
|
10
|
+
@overload
|
11
|
+
async def assert_api_file_exists(dirname: Literal["api"], api_name: str) -> None: ...
|
12
|
+
@overload
|
13
|
+
async def assert_api_file_exists(dirname: Literal["auth-sessions"], api_name: Literal["create", "check"]) -> None: ...
|
14
|
+
|
15
|
+
|
16
|
+
async def assert_api_file_exists(dirname: Literal["api", "auth-sessions"], api_name: str) -> None:
|
17
|
+
"""
|
18
|
+
Assert that the API file exists in the specified folder.
|
19
|
+
"""
|
20
|
+
path = (await Path().resolve()) / dirname / f"{api_name}.py"
|
21
|
+
if not await path.exists():
|
22
|
+
raise CLIError("File does not exist")
|
23
|
+
|
24
|
+
|
25
|
+
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.")
|
29
|
+
try:
|
30
|
+
return IntunedJson.model_validate_json(await intuned_json_path.read_text())
|
31
|
+
except Exception as e:
|
32
|
+
raise CLIError(f"Failed to parse Intuned.json: {e}") from e
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from anyio import Path
|
5
|
+
|
6
|
+
from intuned_cli.utils.api_helpers import load_intuned_json
|
7
|
+
from intuned_cli.utils.error import CLIError
|
8
|
+
|
9
|
+
|
10
|
+
class CLIAssertionError(CLIError):
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
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
|
+
intuned_json = await load_intuned_json()
|
20
|
+
return intuned_json.auth_sessions.enabled
|
21
|
+
|
22
|
+
|
23
|
+
async def assert_auth_enabled():
|
24
|
+
if not await is_auth_enabled():
|
25
|
+
raise CLIAssertionError("Auth session is not enabled, enable it in Intuned.json to use it")
|
26
|
+
|
27
|
+
|
28
|
+
async def assert_auth_consistent(auth_session_id: str | None = None):
|
29
|
+
_is_auth_enabled = await is_auth_enabled()
|
30
|
+
if _is_auth_enabled and auth_session_id is None:
|
31
|
+
raise CLIAssertionError(
|
32
|
+
"Auth session is enabled, but no auth session is provided. Please provide an auth session ID."
|
33
|
+
)
|
34
|
+
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")
|
36
|
+
|
37
|
+
|
38
|
+
async def load_parameters(parameters: str) -> dict[str, Any]:
|
39
|
+
"""
|
40
|
+
Load parameters from a JSON file or a JSON string.
|
41
|
+
If the input is a file path, it reads the file and returns the parsed JSON.
|
42
|
+
If the input is a JSON string, it parses and returns the JSON.
|
43
|
+
"""
|
44
|
+
|
45
|
+
try:
|
46
|
+
# Check if the input is a file path
|
47
|
+
path = Path(parameters)
|
48
|
+
if await path.exists():
|
49
|
+
content = await path.read_text()
|
50
|
+
return json.loads(content)
|
51
|
+
else:
|
52
|
+
# If not a file, treat it as a JSON string
|
53
|
+
return json.loads(parameters)
|
54
|
+
except json.JSONDecodeError as e:
|
55
|
+
raise CLIError(f"Invalid JSON format: {e}") from e
|
56
|
+
except Exception as e:
|
57
|
+
raise CLIError(f"Failed to load parameters: {e}") from e
|
File without changes
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import traceback
|
2
|
+
|
3
|
+
from intuned_cli.utils.console import console
|
4
|
+
from runtime.errors.run_api_errors import AutomationError
|
5
|
+
|
6
|
+
|
7
|
+
class CLIError(Exception):
|
8
|
+
"""Base class for CLI errors."""
|
9
|
+
|
10
|
+
def __init__(self, message: str, auto_color: bool = True):
|
11
|
+
"""
|
12
|
+
Initialize the CLIError.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
message (str): The error message.
|
16
|
+
auto_color (bool): Whether to automatically color the error message.
|
17
|
+
"""
|
18
|
+
super().__init__(message)
|
19
|
+
self.message = message
|
20
|
+
self.auto_color = auto_color
|
21
|
+
|
22
|
+
|
23
|
+
def log_automation_error(e: AutomationError):
|
24
|
+
console.print("[bold red]An error occurred while running the API:[/bold red]")
|
25
|
+
|
26
|
+
stack_trace = traceback.format_exception(type(e.error), value=e.error, tb=e.error.__traceback__)
|
27
|
+
console.print(f"[red]{''.join(stack_trace)}[/red]")
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Default exclude patterns (always applied regardless of .gitignore) for init and deploy commands in Python projects
|
2
|
+
exclusions = [
|
3
|
+
"__pycache__/**",
|
4
|
+
".git/**",
|
5
|
+
"dist/**",
|
6
|
+
"build/**",
|
7
|
+
".eggs/**",
|
8
|
+
"*.egg-info/**",
|
9
|
+
"*.egg",
|
10
|
+
".pytest_cache/**",
|
11
|
+
".mypy_cache/**",
|
12
|
+
".coverage",
|
13
|
+
"htmlcov/**",
|
14
|
+
".tox/**",
|
15
|
+
".venv/**",
|
16
|
+
"venv/**",
|
17
|
+
"env/**",
|
18
|
+
".env",
|
19
|
+
".env.*",
|
20
|
+
".DS_Store",
|
21
|
+
"*.log",
|
22
|
+
"*.pyc",
|
23
|
+
"*.pyo",
|
24
|
+
"*.pyd",
|
25
|
+
"*.sqlite3",
|
26
|
+
"*.db",
|
27
|
+
"*.db-journal",
|
28
|
+
"*.swp",
|
29
|
+
"*.swo",
|
30
|
+
"*.bak",
|
31
|
+
"*.tmp",
|
32
|
+
"pip-log.txt",
|
33
|
+
"pip-delete-this-directory.txt",
|
34
|
+
"README.md",
|
35
|
+
"output/**",
|
36
|
+
"tmp/**",
|
37
|
+
]
|
38
|
+
|
39
|
+
# For compatibility with import style similar to 'export default exclusions'
|
40
|
+
__all__ = ["exclusions"]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from runtime.context.context import IntunedContext
|
4
|
+
|
5
|
+
|
6
|
+
def register_get_auth_session_parameters(auth_session_id: str | None = None):
|
7
|
+
async def get_auth_session_parameters() -> dict[str, Any]:
|
8
|
+
if auth_session_id is None:
|
9
|
+
raise ValueError("get_auth_session_parameters cannot be called without using an auth session")
|
10
|
+
|
11
|
+
from intuned_cli.controller.authsession import load_auth_session_instance
|
12
|
+
|
13
|
+
_, metadata = await load_auth_session_instance(auth_session_id)
|
14
|
+
if metadata.auth_session_type == "MANUAL":
|
15
|
+
raise ValueError("Auth session is recorder-based, it does not have parameters.")
|
16
|
+
return metadata.auth_session_input or {}
|
17
|
+
|
18
|
+
IntunedContext.current().get_auth_session_parameters = get_auth_session_parameters
|