aidp-cli 1.0.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.
- aidp_cli/__init__.py +3 -0
- aidp_cli/body_security.py +166 -0
- aidp_cli/discovery.py +34 -0
- aidp_cli/main.py +2006 -0
- aidp_cli/manifest.py +229 -0
- aidp_cli/operation_manifest.json +29073 -0
- aidp_cli-1.0.0.dist-info/METADATA +10 -0
- aidp_cli-1.0.0.dist-info/RECORD +13 -0
- aidp_cli-1.0.0.dist-info/WHEEL +5 -0
- aidp_cli-1.0.0.dist-info/entry_points.txt +2 -0
- aidp_cli-1.0.0.dist-info/licenses/LICENSE.txt +35 -0
- aidp_cli-1.0.0.dist-info/licenses/NOTICE.txt +1 -0
- aidp_cli-1.0.0.dist-info/top_level.txt +1 -0
aidp_cli/main.py
ADDED
|
@@ -0,0 +1,2006 @@
|
|
|
1
|
+
# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import textwrap
|
|
12
|
+
import uuid
|
|
13
|
+
import warnings
|
|
14
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
15
|
+
from json import JSONDecodeError
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import SimpleNamespace
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
from urllib.parse import quote
|
|
20
|
+
|
|
21
|
+
import oci
|
|
22
|
+
from oci.auth.signers import SecurityTokenSigner
|
|
23
|
+
from oci.exceptions import ConfigFileNotFound, InvalidConfig, RequestException, ServiceError
|
|
24
|
+
from oci.signer import Signer, load_private_key_from_file
|
|
25
|
+
from oci.util import to_dict
|
|
26
|
+
from oci._vendor.requests.exceptions import RequestException as VendorRequestException
|
|
27
|
+
|
|
28
|
+
from aidp_cli.body_security import (
|
|
29
|
+
body_contains_sensitive_field,
|
|
30
|
+
command_has_sensitive_body_fields,
|
|
31
|
+
json_body_argument_source,
|
|
32
|
+
)
|
|
33
|
+
from aidp_cli.discovery import discover_clients
|
|
34
|
+
from aidp_cli.manifest import (
|
|
35
|
+
BodyField,
|
|
36
|
+
BodyModel,
|
|
37
|
+
CommandDefinition,
|
|
38
|
+
CommandField,
|
|
39
|
+
CommandGroup,
|
|
40
|
+
CommandManifest,
|
|
41
|
+
find_command,
|
|
42
|
+
find_command_group,
|
|
43
|
+
load_command_manifest,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
warnings.filterwarnings(
|
|
48
|
+
"ignore",
|
|
49
|
+
message=r"The 'strict' parameter is no longer needed on Python 3\+.",
|
|
50
|
+
category=FutureWarning,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
DEFAULT_PROFILE = "DEFAULT"
|
|
54
|
+
DEFAULT_AUTH = "security_token"
|
|
55
|
+
PACKAGE_NAME = "aidp-cli"
|
|
56
|
+
DEFAULT_ENVIRONMENT_PREFIX = "aidp"
|
|
57
|
+
DEFAULT_ENVIRONMENT_DOMAIN = "oraclecloud.com"
|
|
58
|
+
MAX_BODY_EXAMPLE_DEPTH = 12
|
|
59
|
+
AUTH_CHOICES = ("api_key", "security_token", "instance_principal", "resource_principal")
|
|
60
|
+
SEARCH_ACTION_PREFIXES = (
|
|
61
|
+
"checkout",
|
|
62
|
+
"commit",
|
|
63
|
+
"create",
|
|
64
|
+
"delete",
|
|
65
|
+
"deploy",
|
|
66
|
+
"download",
|
|
67
|
+
"export",
|
|
68
|
+
"generate",
|
|
69
|
+
"head",
|
|
70
|
+
"list",
|
|
71
|
+
"manage",
|
|
72
|
+
"merge",
|
|
73
|
+
"patch",
|
|
74
|
+
"pull",
|
|
75
|
+
"push",
|
|
76
|
+
"rebase",
|
|
77
|
+
"remove",
|
|
78
|
+
"restart",
|
|
79
|
+
"restore",
|
|
80
|
+
"search",
|
|
81
|
+
"start",
|
|
82
|
+
"stop",
|
|
83
|
+
"summarize",
|
|
84
|
+
"update",
|
|
85
|
+
"upload",
|
|
86
|
+
"get",
|
|
87
|
+
)
|
|
88
|
+
CONFIG_ENV_OVERRIDES = {
|
|
89
|
+
"OCI_CLI_USER": "user",
|
|
90
|
+
"OCI_CLI_FINGERPRINT": "fingerprint",
|
|
91
|
+
"OCI_CLI_KEY_FILE": "key_file",
|
|
92
|
+
"OCI_CLI_KEY_CONTENT": "key_content",
|
|
93
|
+
"OCI_CLI_TENANCY": "tenancy",
|
|
94
|
+
"OCI_CLI_PASSPHRASE": "pass_phrase",
|
|
95
|
+
"OCI_CLI_SECURITY_TOKEN_FILE": "security_token_file",
|
|
96
|
+
}
|
|
97
|
+
GLOBAL_VALUE_OPTIONS = {
|
|
98
|
+
"--config-file": "config_file",
|
|
99
|
+
"--profile": "profile",
|
|
100
|
+
"-p": "profile",
|
|
101
|
+
"--auth": "auth",
|
|
102
|
+
"--region": "region",
|
|
103
|
+
"--endpoint": "endpoint",
|
|
104
|
+
"--environment-prefix": "environment_prefix",
|
|
105
|
+
"--environmentprefix": "environment_prefix",
|
|
106
|
+
"--environment-domain": "environment_domain",
|
|
107
|
+
"--environment-host": "environment_host",
|
|
108
|
+
"--timeout": "timeout",
|
|
109
|
+
"--instance-id": "ai_data_platform_id",
|
|
110
|
+
}
|
|
111
|
+
GLOBAL_BOOLEAN_OPTIONS = {
|
|
112
|
+
"--debug": "debug",
|
|
113
|
+
}
|
|
114
|
+
SKIPPED_COMMAND_FIELDS = {"opc_request_id"}
|
|
115
|
+
STRICT_URL_ENCODING_CLIENTS = {"NotebookClient"}
|
|
116
|
+
UTILITY_COMMANDS = (
|
|
117
|
+
("command-groups", "List API command groups."),
|
|
118
|
+
("search", "Search command groups and command names."),
|
|
119
|
+
("configure", "Show AIDP CLI environment defaults."),
|
|
120
|
+
("help", "Help about any command."),
|
|
121
|
+
("version", "Show CLI version."),
|
|
122
|
+
)
|
|
123
|
+
CLIENTS: dict[str, type] = {}
|
|
124
|
+
MANIFEST: CommandManifest | None = None
|
|
125
|
+
AIDP_DP_MODULE: Any = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class CliError(Exception):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main(argv: list[str] | None = None) -> int:
|
|
133
|
+
raw_argv = sys.argv[1:] if argv is None else argv
|
|
134
|
+
try:
|
|
135
|
+
globals_ns, args, help_requested, version_requested = parse_global_options(raw_argv)
|
|
136
|
+
if version_requested:
|
|
137
|
+
print_version()
|
|
138
|
+
return 0
|
|
139
|
+
ensure_manifest_loaded()
|
|
140
|
+
if not args:
|
|
141
|
+
print_root_help()
|
|
142
|
+
return 0
|
|
143
|
+
return dispatch(args, globals_ns, help_requested)
|
|
144
|
+
except CliError as exc:
|
|
145
|
+
print(f"aidp: error: {exc}", file=sys.stderr)
|
|
146
|
+
return 2
|
|
147
|
+
except (ConfigFileNotFound, InvalidConfig) as exc:
|
|
148
|
+
print(f"aidp: error: OCI config error: {exc}", file=sys.stderr)
|
|
149
|
+
return 2
|
|
150
|
+
except FileNotFoundError as exc:
|
|
151
|
+
print(f"aidp: error: file not found: {exc.filename}", file=sys.stderr)
|
|
152
|
+
return 2
|
|
153
|
+
except PermissionError as exc:
|
|
154
|
+
print(f"aidp: error: permission denied: {exc.filename}", file=sys.stderr)
|
|
155
|
+
return 2
|
|
156
|
+
except JSONDecodeError as exc:
|
|
157
|
+
print(f"aidp: error: invalid JSON: {exc}", file=sys.stderr)
|
|
158
|
+
return 2
|
|
159
|
+
except VendorRequestException as exc:
|
|
160
|
+
print(f"aidp: error: request failed: {exc}", file=sys.stderr)
|
|
161
|
+
return 1
|
|
162
|
+
except RequestException as exc:
|
|
163
|
+
print(f"aidp: error: request failed: {exc}", file=sys.stderr)
|
|
164
|
+
return 1
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
print(f"aidp: error: file error: {exc}", file=sys.stderr)
|
|
167
|
+
return 2
|
|
168
|
+
except ValueError as exc:
|
|
169
|
+
print(f"aidp: error: {exc}", file=sys.stderr)
|
|
170
|
+
return 2
|
|
171
|
+
except ServiceError as exc:
|
|
172
|
+
print_error_response(
|
|
173
|
+
{
|
|
174
|
+
"status": exc.status,
|
|
175
|
+
"code": exc.code,
|
|
176
|
+
"message": exc.message,
|
|
177
|
+
"opc-request-id": exc.request_id,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return int(exc.status or 1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def ensure_manifest_loaded() -> None:
|
|
184
|
+
global MANIFEST
|
|
185
|
+
if MANIFEST is not None:
|
|
186
|
+
return
|
|
187
|
+
try:
|
|
188
|
+
MANIFEST = load_command_manifest()
|
|
189
|
+
except RuntimeError as exc:
|
|
190
|
+
raise CliError(str(exc)) from exc
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def ensure_clients_loaded() -> None:
|
|
194
|
+
global AIDP_DP_MODULE, CLIENTS
|
|
195
|
+
if AIDP_DP_MODULE is not None:
|
|
196
|
+
return
|
|
197
|
+
try:
|
|
198
|
+
from aidp_python_client import aidataplatform_dp as sdk_module
|
|
199
|
+
except ImportError as exc:
|
|
200
|
+
raise CliError(
|
|
201
|
+
f"Unable to import aidp-python-client SDK: {exc}. Install the generated SDK wheel with aidp-cli."
|
|
202
|
+
) from exc
|
|
203
|
+
AIDP_DP_MODULE = sdk_module
|
|
204
|
+
CLIENTS = discover_clients(sdk_module)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def command_manifest() -> CommandManifest:
|
|
208
|
+
if MANIFEST is None:
|
|
209
|
+
raise CliError("AIDP command manifest is not loaded.")
|
|
210
|
+
return MANIFEST
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def dispatch(args: list[str], globals_ns: SimpleNamespace, help_requested: bool) -> int:
|
|
214
|
+
command = args[0]
|
|
215
|
+
tail = args[1:]
|
|
216
|
+
|
|
217
|
+
if command == "help":
|
|
218
|
+
return handle_help(tail, globals_ns)
|
|
219
|
+
if command == "version":
|
|
220
|
+
print_version()
|
|
221
|
+
return 0
|
|
222
|
+
if command == "command-groups":
|
|
223
|
+
if help_requested:
|
|
224
|
+
print_command_groups_help()
|
|
225
|
+
else:
|
|
226
|
+
print_command_groups()
|
|
227
|
+
return 0
|
|
228
|
+
if command == "search":
|
|
229
|
+
if help_requested or not tail:
|
|
230
|
+
print_search_help()
|
|
231
|
+
else:
|
|
232
|
+
handle_search(tail)
|
|
233
|
+
return 0
|
|
234
|
+
if command == "configure":
|
|
235
|
+
if help_requested or not tail:
|
|
236
|
+
print_configure_help()
|
|
237
|
+
else:
|
|
238
|
+
handle_configure(tail)
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
group = find_command_group(command_manifest(), command)
|
|
242
|
+
if group is None:
|
|
243
|
+
raise unknown_command_group(command)
|
|
244
|
+
globals_ns, tail = consume_leading_global_options(tail, globals_ns)
|
|
245
|
+
if not tail:
|
|
246
|
+
print_group_help(group)
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
command_name = tail[0]
|
|
250
|
+
command_def = find_command(group, command_name)
|
|
251
|
+
if command_def is None:
|
|
252
|
+
raise unknown_command_name(group, command_name)
|
|
253
|
+
if help_requested:
|
|
254
|
+
print_command_help(group, command_def)
|
|
255
|
+
return 0
|
|
256
|
+
return handle_command(group, command_def, tail[1:], globals_ns)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def handle_help(args: list[str], globals_ns: SimpleNamespace) -> int:
|
|
260
|
+
if not args:
|
|
261
|
+
print_root_help()
|
|
262
|
+
return 0
|
|
263
|
+
group = find_command_group(command_manifest(), args[0])
|
|
264
|
+
if group is None:
|
|
265
|
+
if args[0] == "command-groups":
|
|
266
|
+
print_command_groups_help()
|
|
267
|
+
return 0
|
|
268
|
+
if args[0] == "search":
|
|
269
|
+
print_search_help()
|
|
270
|
+
return 0
|
|
271
|
+
if args[0] == "configure":
|
|
272
|
+
print_configure_help()
|
|
273
|
+
return 0
|
|
274
|
+
if args[0] == "version":
|
|
275
|
+
print_version()
|
|
276
|
+
return 0
|
|
277
|
+
raise unknown_command_group(args[0])
|
|
278
|
+
if len(args) == 1:
|
|
279
|
+
print_group_help(group)
|
|
280
|
+
return 0
|
|
281
|
+
command_def = find_command(group, args[1])
|
|
282
|
+
if command_def is None:
|
|
283
|
+
raise unknown_command_name(group, args[1])
|
|
284
|
+
print_command_help(group, command_def)
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def parse_global_options(argv: list[str]) -> tuple[SimpleNamespace, list[str], bool, bool]:
|
|
289
|
+
values = global_defaults()
|
|
290
|
+
remaining: list[str] = []
|
|
291
|
+
help_requested = False
|
|
292
|
+
version_requested = False
|
|
293
|
+
index = 0
|
|
294
|
+
command_seen = False
|
|
295
|
+
|
|
296
|
+
while index < len(argv):
|
|
297
|
+
token = argv[index]
|
|
298
|
+
if token in {"-h", "--help"}:
|
|
299
|
+
help_requested = True
|
|
300
|
+
index += 1
|
|
301
|
+
continue
|
|
302
|
+
if token in {"-v", "--version"}:
|
|
303
|
+
version_requested = True
|
|
304
|
+
index += 1
|
|
305
|
+
continue
|
|
306
|
+
if not command_seen and token in GLOBAL_BOOLEAN_OPTIONS:
|
|
307
|
+
values[GLOBAL_BOOLEAN_OPTIONS[token]] = True
|
|
308
|
+
index += 1
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
option_name, inline_value = split_inline_option(token)
|
|
312
|
+
if not command_seen and option_name in GLOBAL_VALUE_OPTIONS:
|
|
313
|
+
value = inline_value
|
|
314
|
+
if value is None:
|
|
315
|
+
if index + 1 >= len(argv):
|
|
316
|
+
raise CliError(f"{option_name} requires a value.")
|
|
317
|
+
value = argv[index + 1]
|
|
318
|
+
index += 1
|
|
319
|
+
apply_global_option(values, option_name, value)
|
|
320
|
+
index += 1
|
|
321
|
+
continue
|
|
322
|
+
if not command_seen and token.startswith("-"):
|
|
323
|
+
raise CliError(f"Unknown option {option_name!r}.")
|
|
324
|
+
|
|
325
|
+
remaining.append(token)
|
|
326
|
+
if not token.startswith("-"):
|
|
327
|
+
command_seen = True
|
|
328
|
+
index += 1
|
|
329
|
+
|
|
330
|
+
validate_global_options(values)
|
|
331
|
+
return SimpleNamespace(**values), remaining, help_requested, version_requested
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def validate_global_options(values: dict[str, Any]) -> None:
|
|
335
|
+
if values["auth"] not in AUTH_CHOICES:
|
|
336
|
+
raise CliError(
|
|
337
|
+
f"argument --auth: invalid choice: {values['auth']!r} "
|
|
338
|
+
f"(choose from {', '.join(AUTH_CHOICES)})"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def consume_leading_global_options(
|
|
343
|
+
tokens: list[str],
|
|
344
|
+
globals_ns: SimpleNamespace,
|
|
345
|
+
) -> tuple[SimpleNamespace, list[str]]:
|
|
346
|
+
values = vars(globals_ns).copy()
|
|
347
|
+
remaining: list[str] = []
|
|
348
|
+
index = 0
|
|
349
|
+
|
|
350
|
+
while index < len(tokens):
|
|
351
|
+
token = tokens[index]
|
|
352
|
+
if token in GLOBAL_BOOLEAN_OPTIONS:
|
|
353
|
+
values[GLOBAL_BOOLEAN_OPTIONS[token]] = True
|
|
354
|
+
index += 1
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
option_name, inline_value = split_inline_option(token)
|
|
358
|
+
if option_name in GLOBAL_VALUE_OPTIONS:
|
|
359
|
+
value = inline_value
|
|
360
|
+
if value is None:
|
|
361
|
+
if index + 1 >= len(tokens):
|
|
362
|
+
raise CliError(f"{option_name} requires a value.")
|
|
363
|
+
value = tokens[index + 1]
|
|
364
|
+
index += 1
|
|
365
|
+
apply_global_option(values, option_name, value)
|
|
366
|
+
index += 1
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
remaining.extend(tokens[index:])
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
validate_global_options(values)
|
|
373
|
+
return SimpleNamespace(**values), remaining
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def global_defaults() -> dict[str, Any]:
|
|
377
|
+
return {
|
|
378
|
+
"config_file": os.getenv("OCI_CLI_CONFIG_FILE", "~/.oci/config"),
|
|
379
|
+
"profile": os.getenv("OCI_CLI_PROFILE", DEFAULT_PROFILE),
|
|
380
|
+
"auth": os.getenv("OCI_CLI_AUTH", DEFAULT_AUTH),
|
|
381
|
+
"region": os.getenv("OCI_CLI_REGION"),
|
|
382
|
+
"endpoint": os.getenv("AIDP_ENDPOINT") or os.getenv("OCI_CLI_ENDPOINT"),
|
|
383
|
+
"environment_prefix": DEFAULT_ENVIRONMENT_PREFIX,
|
|
384
|
+
"environment_domain": DEFAULT_ENVIRONMENT_DOMAIN,
|
|
385
|
+
"environment_host": None,
|
|
386
|
+
"timeout": None,
|
|
387
|
+
"ai_data_platform_id": configured_instance_id(),
|
|
388
|
+
"debug": False,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def apply_global_option(values: dict[str, Any], option_name: str, value: str) -> None:
|
|
393
|
+
key = GLOBAL_VALUE_OPTIONS[option_name]
|
|
394
|
+
if key == "timeout":
|
|
395
|
+
try:
|
|
396
|
+
timeout = float(value)
|
|
397
|
+
except ValueError as exc:
|
|
398
|
+
raise CliError("--timeout must be a positive number of seconds.") from exc
|
|
399
|
+
if timeout <= 0:
|
|
400
|
+
raise CliError("--timeout must be a positive number of seconds.")
|
|
401
|
+
values[key] = timeout
|
|
402
|
+
return
|
|
403
|
+
values[key] = value
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def split_inline_option(token: str) -> tuple[str, str | None]:
|
|
407
|
+
if "=" not in token:
|
|
408
|
+
return token, None
|
|
409
|
+
name, value = token.split("=", 1)
|
|
410
|
+
return name, value
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def print_root_help() -> None:
|
|
414
|
+
lines = [
|
|
415
|
+
"AIDP CLI",
|
|
416
|
+
"",
|
|
417
|
+
"Usage:",
|
|
418
|
+
" aidp <command-group> <command-name> [flags]",
|
|
419
|
+
"",
|
|
420
|
+
"API Command Groups:",
|
|
421
|
+
*format_table((group.name, group.description) for group in command_manifest().command_groups),
|
|
422
|
+
"",
|
|
423
|
+
"Utility Commands:",
|
|
424
|
+
*format_table(UTILITY_COMMANDS),
|
|
425
|
+
"",
|
|
426
|
+
"Flags:",
|
|
427
|
+
*format_flags(root_flag_rows()),
|
|
428
|
+
"",
|
|
429
|
+
'Use "aidp <command-group> --help" for more information about a command group.',
|
|
430
|
+
]
|
|
431
|
+
print("\n".join(lines))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def print_command_groups_help() -> None:
|
|
435
|
+
lines = [
|
|
436
|
+
"List AIDP API command groups.",
|
|
437
|
+
"",
|
|
438
|
+
"Usage:",
|
|
439
|
+
" aidp command-groups [flags]",
|
|
440
|
+
"",
|
|
441
|
+
"Flags:",
|
|
442
|
+
*format_flags([("-h, --help", "help for command-groups")]),
|
|
443
|
+
"",
|
|
444
|
+
"Global Flags:",
|
|
445
|
+
*format_flags(global_flag_rows()),
|
|
446
|
+
]
|
|
447
|
+
print("\n".join(lines))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def print_command_groups() -> None:
|
|
451
|
+
rows = [
|
|
452
|
+
(group.name, group.description)
|
|
453
|
+
for group in command_manifest().command_groups
|
|
454
|
+
]
|
|
455
|
+
print("Command Groups:")
|
|
456
|
+
print("\n".join(format_table(rows)))
|
|
457
|
+
print()
|
|
458
|
+
print('Use "aidp <command-group> --help" for command names and examples.')
|
|
459
|
+
example = runnable_command_groups_example()
|
|
460
|
+
if example:
|
|
461
|
+
print()
|
|
462
|
+
print("Example:")
|
|
463
|
+
print(example)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def print_group_help(group: CommandGroup) -> None:
|
|
467
|
+
lines = [
|
|
468
|
+
group.description,
|
|
469
|
+
"",
|
|
470
|
+
"Usage:",
|
|
471
|
+
f" aidp {group.name} [flags]",
|
|
472
|
+
f" aidp {group.name} <command-name> [arguments] [flags]",
|
|
473
|
+
"",
|
|
474
|
+
]
|
|
475
|
+
for section, commands in grouped_commands(group).items():
|
|
476
|
+
lines.append(section + ":")
|
|
477
|
+
lines.extend(
|
|
478
|
+
format_table((command.name, command_summary(command)) for command in commands)
|
|
479
|
+
)
|
|
480
|
+
lines.append("")
|
|
481
|
+
lines.extend(
|
|
482
|
+
[
|
|
483
|
+
"Flags:",
|
|
484
|
+
*format_flags([("-h, --help", f"help for {group.name}")]),
|
|
485
|
+
"",
|
|
486
|
+
"Global Flags:",
|
|
487
|
+
*format_flags(global_flag_rows()),
|
|
488
|
+
"",
|
|
489
|
+
f'Use "aidp {group.name} <command-name> --help" for more information about a command.',
|
|
490
|
+
]
|
|
491
|
+
)
|
|
492
|
+
print("\n".join(lines))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def print_command_help(group: CommandGroup, command: CommandDefinition) -> None:
|
|
496
|
+
description = command_description(command)
|
|
497
|
+
lines = []
|
|
498
|
+
if description:
|
|
499
|
+
lines.extend(wrap_paragraph(description))
|
|
500
|
+
lines.append("")
|
|
501
|
+
|
|
502
|
+
argument_fields = command_argument_fields(command)
|
|
503
|
+
usage_args = " ".join(argument_metavar(field) for field in argument_fields)
|
|
504
|
+
usage_args = f" {usage_args}" if usage_args else ""
|
|
505
|
+
lines.extend(
|
|
506
|
+
[
|
|
507
|
+
"Usage:",
|
|
508
|
+
f" aidp {group.name} {command.name}{usage_args} [flags]",
|
|
509
|
+
"",
|
|
510
|
+
]
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if argument_fields:
|
|
514
|
+
lines.append("Arguments:")
|
|
515
|
+
for field in argument_fields:
|
|
516
|
+
lines.extend(format_argument(field))
|
|
517
|
+
lines.append("")
|
|
518
|
+
|
|
519
|
+
lines.append("Flags:")
|
|
520
|
+
lines.extend(format_flags(command_flag_rows(command)))
|
|
521
|
+
lines.append("")
|
|
522
|
+
lines.append("Global Flags:")
|
|
523
|
+
lines.extend(format_flags(global_flag_rows()))
|
|
524
|
+
lines.append("")
|
|
525
|
+
lines.append("Examples:")
|
|
526
|
+
lines.extend(format_examples(group, command))
|
|
527
|
+
|
|
528
|
+
body_sample = example_body(command)
|
|
529
|
+
if body_sample is not None:
|
|
530
|
+
lines.extend(["", "Example JSON:", json.dumps(body_sample, indent=2)])
|
|
531
|
+
body_variants = root_body_variant_examples(command)
|
|
532
|
+
if body_variants:
|
|
533
|
+
lines.extend(["", "Body variants:"])
|
|
534
|
+
for label, sample in body_variants:
|
|
535
|
+
lines.extend(["", f"Example JSON - {label}:", json.dumps(sample, indent=2)])
|
|
536
|
+
nested_variants = nested_body_variant_examples(command)
|
|
537
|
+
if nested_variants:
|
|
538
|
+
lines.extend(["", "Nested body variants:"])
|
|
539
|
+
for path, label, sample in nested_variants:
|
|
540
|
+
lines.extend(["", f"Example JSON for {path} - {label}:", json.dumps(sample, indent=2)])
|
|
541
|
+
required_fields = body_required_fields(command)
|
|
542
|
+
if required_fields:
|
|
543
|
+
lines.extend(["", "Required JSON fields:"])
|
|
544
|
+
lines.extend(f" {field}" for field in required_fields)
|
|
545
|
+
enum_fields = body_enum_fields(command)
|
|
546
|
+
if enum_fields:
|
|
547
|
+
lines.extend(["", "Allowed JSON values:"])
|
|
548
|
+
for field, values in sorted(enum_fields.items()):
|
|
549
|
+
lines.append(f" {field}: {', '.join(str(value) for value in values)}")
|
|
550
|
+
print("\n".join(lines))
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def grouped_commands(group: CommandGroup) -> dict[str, list[CommandDefinition]]:
|
|
554
|
+
sections: dict[str, list[CommandDefinition]] = {}
|
|
555
|
+
for command in group.commands:
|
|
556
|
+
sections.setdefault(command.section or "Available Commands", []).append(command)
|
|
557
|
+
if "Available Commands" in sections:
|
|
558
|
+
ordered = {"Available Commands": sections.pop("Available Commands")}
|
|
559
|
+
ordered.update(dict(sorted(sections.items())))
|
|
560
|
+
return ordered
|
|
561
|
+
return dict(sorted(sections.items()))
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def command_summary(command: CommandDefinition) -> str:
|
|
565
|
+
summary = command.summary or command.description
|
|
566
|
+
return first_sentence(summary)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def command_description(command: CommandDefinition) -> str:
|
|
570
|
+
description = command.description or command.summary
|
|
571
|
+
if not description:
|
|
572
|
+
return ""
|
|
573
|
+
description = description if description.endswith((".", "!", "?")) else f"{description}."
|
|
574
|
+
if command.deprecated and not description.lower().startswith("deprecated"):
|
|
575
|
+
description = f"Deprecated. {description}"
|
|
576
|
+
return description
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def first_sentence(value: str) -> str:
|
|
580
|
+
cleaned = " ".join(str(value or "").split())
|
|
581
|
+
if not cleaned:
|
|
582
|
+
return ""
|
|
583
|
+
for delimiter in (". ", "! ", "? "):
|
|
584
|
+
if delimiter in cleaned:
|
|
585
|
+
return cleaned.split(delimiter, 1)[0].rstrip(".!?") + "."
|
|
586
|
+
return cleaned if cleaned.endswith((".", "!", "?")) else f"{cleaned}."
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def command_argument_fields(command: CommandDefinition) -> list[CommandField]:
|
|
590
|
+
return [
|
|
591
|
+
field
|
|
592
|
+
for field in command.fields
|
|
593
|
+
if field.location == "path" and field.required and field.name != "ai_data_platform_id"
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def command_option_fields(command: CommandDefinition) -> list[CommandField]:
|
|
598
|
+
argument_names = {field.name for field in command_argument_fields(command)}
|
|
599
|
+
return [
|
|
600
|
+
field
|
|
601
|
+
for field in command.fields
|
|
602
|
+
if field.location != "body"
|
|
603
|
+
and field.name not in SKIPPED_COMMAND_FIELDS
|
|
604
|
+
and field.name != "ai_data_platform_id"
|
|
605
|
+
and field.name not in argument_names
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def command_flag_rows(command: CommandDefinition) -> list[tuple[str, str]]:
|
|
610
|
+
rows = [("-h, --help", f"help for {command.name}")]
|
|
611
|
+
for field in command_option_fields(command):
|
|
612
|
+
rows.append((f"--{field.cli_name}", field_help(field)))
|
|
613
|
+
if command.body_field is not None:
|
|
614
|
+
body_help = (
|
|
615
|
+
"inline string, @path/to/file, file:///path, or - for stdin"
|
|
616
|
+
if command_uses_raw_body(command)
|
|
617
|
+
else (
|
|
618
|
+
"@path/to/file.json, file:///path/request.json, or - for stdin; inline JSON is blocked when it contains sensitive fields"
|
|
619
|
+
if command_has_sensitive_body_fields(command)
|
|
620
|
+
else "inline JSON string, @path/to/file.json, file:///path/request.json, or - for stdin"
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
rows.append(
|
|
624
|
+
(
|
|
625
|
+
"--body",
|
|
626
|
+
body_help,
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
rows.extend(
|
|
630
|
+
[
|
|
631
|
+
("--opc-request-id", "request ID; generated automatically when omitted"),
|
|
632
|
+
("--no-request-id", "do not add opc_request_id automatically"),
|
|
633
|
+
]
|
|
634
|
+
)
|
|
635
|
+
return rows
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def root_flag_rows() -> list[tuple[str, str]]:
|
|
639
|
+
return [
|
|
640
|
+
("--debug", "enable debug logging"),
|
|
641
|
+
("-h, --help", "help for aidp"),
|
|
642
|
+
("-p, --profile", "OCI config profile; the default profile: DEFAULT"),
|
|
643
|
+
("--auth", "OCI authentication mode; options: api_key, security_token, instance_principal, resource_principal; default: security_token"),
|
|
644
|
+
("--config-file", "OCI config file path"),
|
|
645
|
+
("--region", "OCI region"),
|
|
646
|
+
("--endpoint", "AIDP data plane endpoint override; default endpoint points to https://aidp.<region>.oci.oraclecloud.com"),
|
|
647
|
+
("--instance-id", "AIDP instance OCID"),
|
|
648
|
+
("--timeout", "connection/read timeout in seconds"),
|
|
649
|
+
("-v, --version", "version for aidp"),
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def global_flag_rows() -> list[tuple[str, str]]:
|
|
654
|
+
return [
|
|
655
|
+
("--debug", "enable debug logging"),
|
|
656
|
+
("-p, --profile", "OCI config profile; the default profile: DEFAULT"),
|
|
657
|
+
("--auth", "OCI authentication mode; options: api_key, security_token, instance_principal, resource_principal; default: security_token"),
|
|
658
|
+
("--config-file", "OCI config file path"),
|
|
659
|
+
("--region", "OCI region"),
|
|
660
|
+
("--endpoint", "AIDP data plane endpoint override; default endpoint points to https://aidp.<region>.oci.oraclecloud.com"),
|
|
661
|
+
("--instance-id", "AIDP instance OCID"),
|
|
662
|
+
("--timeout", "connection/read timeout in seconds"),
|
|
663
|
+
]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def format_table(rows: Any, indent: int = 2, gap: int = 2) -> list[str]:
|
|
667
|
+
rows = [(name, description) for name, description in rows]
|
|
668
|
+
if not rows:
|
|
669
|
+
return []
|
|
670
|
+
width = max(len(name) for name, _description in rows)
|
|
671
|
+
formatted = []
|
|
672
|
+
for name, description in rows:
|
|
673
|
+
padding = " " * max(gap, width - len(name) + gap)
|
|
674
|
+
formatted.append(f"{' ' * indent}{name}{padding}{description}")
|
|
675
|
+
return formatted
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def format_flags(rows: list[tuple[str, str]]) -> list[str]:
|
|
679
|
+
return format_table(rows, indent=2, gap=2)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def wrap_paragraph(value: str, indent: int = 0) -> list[str]:
|
|
683
|
+
prefix = " " * indent
|
|
684
|
+
return textwrap.wrap(value, width=88, initial_indent=prefix, subsequent_indent=prefix) or [prefix]
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def format_argument(field: CommandField) -> list[str]:
|
|
688
|
+
label = f" {argument_metavar(field)}:"
|
|
689
|
+
description = field.description or "Required command argument."
|
|
690
|
+
wrapped = textwrap.wrap(description, width=78, initial_indent=label + " ", subsequent_indent=" " * len(label) + " ")
|
|
691
|
+
return wrapped or [label]
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def field_help(field: CommandField) -> str:
|
|
695
|
+
parts = []
|
|
696
|
+
description = field.description
|
|
697
|
+
if field.enum_values:
|
|
698
|
+
description = strip_allowed_values_description(description)
|
|
699
|
+
if description:
|
|
700
|
+
parts.append(description.rstrip(".") + ".")
|
|
701
|
+
if field.required:
|
|
702
|
+
parts.append("Required.")
|
|
703
|
+
if field.enum_values:
|
|
704
|
+
parts.append("Allowed values: " + ", ".join(str(value) for value in field.enum_values) + ".")
|
|
705
|
+
return " ".join(parts) if parts else ("Required." if field.required else "")
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def strip_allowed_values_description(value: str) -> str:
|
|
709
|
+
return re.sub(
|
|
710
|
+
r"\s*Allowed values(?:\s+are)?:\s*[^.]+\.?$",
|
|
711
|
+
"",
|
|
712
|
+
value.strip(),
|
|
713
|
+
flags=re.IGNORECASE,
|
|
714
|
+
).strip()
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def argument_metavar(field: CommandField) -> str:
|
|
718
|
+
return field.cli_name.replace("-", "_").upper()
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def format_examples(group: CommandGroup, command: CommandDefinition) -> list[str]:
|
|
722
|
+
base = f" aidp {group.name} {command.name}"
|
|
723
|
+
for field in command_argument_fields(command):
|
|
724
|
+
base += f" <{field.cli_name.replace('-', '_')}>"
|
|
725
|
+
if command_requires_instance_id(command):
|
|
726
|
+
base += " --instance-id <ocid>"
|
|
727
|
+
if command.body_field is not None:
|
|
728
|
+
base += " --body @request.json"
|
|
729
|
+
return [base]
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def runnable_command_groups_example() -> str:
|
|
733
|
+
candidates: list[tuple[tuple[int, int, int, str, str], CommandGroup, CommandDefinition]] = []
|
|
734
|
+
for group in command_manifest().command_groups:
|
|
735
|
+
for command in group.commands:
|
|
736
|
+
argument_count = len(command_argument_fields(command))
|
|
737
|
+
body_count = 1 if command.body_field is not None else 0
|
|
738
|
+
action_rank = 0 if command.name.startswith("list-") else 1
|
|
739
|
+
candidates.append(
|
|
740
|
+
(
|
|
741
|
+
(body_count, argument_count, action_rank, group.name, command.name),
|
|
742
|
+
group,
|
|
743
|
+
command,
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
if not candidates:
|
|
748
|
+
return ""
|
|
749
|
+
_rank, group, command = min(candidates, key=lambda candidate: candidate[0])
|
|
750
|
+
return format_examples(group, command)[0]
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def command_requires_instance_id(command: CommandDefinition) -> bool:
|
|
754
|
+
return any(field.name == "ai_data_platform_id" and field.required for field in command.fields)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def handle_command(
|
|
758
|
+
group: CommandGroup,
|
|
759
|
+
command: CommandDefinition,
|
|
760
|
+
tokens: list[str],
|
|
761
|
+
globals_ns: SimpleNamespace,
|
|
762
|
+
) -> int:
|
|
763
|
+
try:
|
|
764
|
+
invocation = parse_command_options(group, command, tokens, globals_ns)
|
|
765
|
+
except CliError as exc:
|
|
766
|
+
raise with_command_usage_hint(group, command, exc) from exc
|
|
767
|
+
|
|
768
|
+
ensure_clients_loaded()
|
|
769
|
+
client_cls = CLIENTS.get(group.name)
|
|
770
|
+
if client_cls is None:
|
|
771
|
+
client_cls = getattr(AIDP_DP_MODULE, group.client_class_name, None)
|
|
772
|
+
if client_cls is None:
|
|
773
|
+
raise CliError(f"Unknown command group {group.name!r}. Run 'aidp command-groups'.")
|
|
774
|
+
sdk_method = getattr(client_cls, command.sdk_method_name, None)
|
|
775
|
+
if sdk_method is None:
|
|
776
|
+
raise CliError(f"{group.name} has no command-name {command.name!r}.")
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
call_args, call_kwargs = bind_command_args(
|
|
780
|
+
sdk_method=sdk_method,
|
|
781
|
+
params=invocation.params,
|
|
782
|
+
body=invocation.body,
|
|
783
|
+
body_param_name=command.body_field.name if command.body_field else None,
|
|
784
|
+
)
|
|
785
|
+
except CliError as exc:
|
|
786
|
+
raise with_command_usage_hint(group, command, exc) from exc
|
|
787
|
+
add_request_id(sdk_method, command.name, call_kwargs, invocation)
|
|
788
|
+
client = build_client(client_cls, invocation)
|
|
789
|
+
if invocation.debug:
|
|
790
|
+
enable_request_debug(client, group.name, command.name)
|
|
791
|
+
try:
|
|
792
|
+
response = sdk_method(client, *call_args, **call_kwargs)
|
|
793
|
+
print_response(response)
|
|
794
|
+
finally:
|
|
795
|
+
close_client(client)
|
|
796
|
+
return 0
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def parse_command_options(
|
|
800
|
+
group: CommandGroup,
|
|
801
|
+
command: CommandDefinition,
|
|
802
|
+
tokens: list[str],
|
|
803
|
+
globals_ns: SimpleNamespace,
|
|
804
|
+
) -> SimpleNamespace:
|
|
805
|
+
params: dict[str, Any] = {}
|
|
806
|
+
body = None
|
|
807
|
+
opc_request_id = None
|
|
808
|
+
no_request_id = False
|
|
809
|
+
positionals: list[str] = []
|
|
810
|
+
option_fields = {f"--{field.cli_name}": field for field in command_option_fields(command)}
|
|
811
|
+
global_values = vars(globals_ns).copy()
|
|
812
|
+
index = 0
|
|
813
|
+
|
|
814
|
+
while index < len(tokens):
|
|
815
|
+
token = tokens[index]
|
|
816
|
+
option_name, inline_value = split_inline_option(token)
|
|
817
|
+
if option_name == "--body":
|
|
818
|
+
if command.body_field is None:
|
|
819
|
+
raise command_usage_error(group, command, f"{command.name} does not accept --body.")
|
|
820
|
+
value, index = command_option_value(tokens, index, option_name, inline_value)
|
|
821
|
+
body = load_body_input(command, value)
|
|
822
|
+
if (
|
|
823
|
+
not command_uses_raw_body(command)
|
|
824
|
+
and json_body_argument_source(value) == "inline"
|
|
825
|
+
and body_contains_sensitive_field(command, body)
|
|
826
|
+
):
|
|
827
|
+
raise command_usage_error(
|
|
828
|
+
group,
|
|
829
|
+
command,
|
|
830
|
+
"Inline --body JSON is blocked because this request body contains sensitive fields. "
|
|
831
|
+
"Use --body @request.json, --body file:///path/request.json, or --body -.",
|
|
832
|
+
)
|
|
833
|
+
continue
|
|
834
|
+
if option_name == "--opc-request-id":
|
|
835
|
+
opc_request_id, index = command_option_value(tokens, index, option_name, inline_value)
|
|
836
|
+
continue
|
|
837
|
+
if option_name == "--no-request-id":
|
|
838
|
+
no_request_id = True
|
|
839
|
+
index += 1
|
|
840
|
+
continue
|
|
841
|
+
if option_name in option_fields:
|
|
842
|
+
field = option_fields[option_name]
|
|
843
|
+
value, index = field_option_value(tokens, index, option_name, inline_value, field)
|
|
844
|
+
params[field.name] = value if isinstance(value, bool) else parse_value(str(value))
|
|
845
|
+
continue
|
|
846
|
+
if option_name in GLOBAL_VALUE_OPTIONS:
|
|
847
|
+
value, index = command_option_value(tokens, index, option_name, inline_value)
|
|
848
|
+
apply_global_option(global_values, option_name, value)
|
|
849
|
+
continue
|
|
850
|
+
if option_name in GLOBAL_BOOLEAN_OPTIONS:
|
|
851
|
+
global_values[GLOBAL_BOOLEAN_OPTIONS[option_name]] = True
|
|
852
|
+
index += 1
|
|
853
|
+
continue
|
|
854
|
+
if token.startswith("-"):
|
|
855
|
+
raise unknown_option(group, command, option_name, option_fields)
|
|
856
|
+
positionals.append(token)
|
|
857
|
+
index += 1
|
|
858
|
+
|
|
859
|
+
validate_global_options(global_values)
|
|
860
|
+
|
|
861
|
+
argument_fields = command_argument_fields(command)
|
|
862
|
+
if len(positionals) < len(argument_fields):
|
|
863
|
+
missing = argument_metavar(argument_fields[len(positionals)])
|
|
864
|
+
raise command_usage_error(group, command, f"Missing required argument {missing}.")
|
|
865
|
+
if len(positionals) > len(argument_fields):
|
|
866
|
+
raise command_usage_error(group, command, f"Unexpected argument {positionals[len(argument_fields)]!r}.")
|
|
867
|
+
for field, value in zip(argument_fields, positionals):
|
|
868
|
+
params[field.name] = parse_value(value)
|
|
869
|
+
|
|
870
|
+
for field in command_option_fields(command):
|
|
871
|
+
if field.required and field.name not in params:
|
|
872
|
+
raise command_usage_error(group, command, f"Missing required flag --{field.cli_name}.")
|
|
873
|
+
if command.body_field is not None and command.body_field.required and body is None:
|
|
874
|
+
raise command_usage_error(group, command, "Missing required flag --body.")
|
|
875
|
+
if command_requires_instance_id(command):
|
|
876
|
+
if not global_values["ai_data_platform_id"]:
|
|
877
|
+
raise command_usage_error(
|
|
878
|
+
group,
|
|
879
|
+
command,
|
|
880
|
+
"Missing AIDP instance OCID. Set --instance-id or AIDP_INSTANCE_ID.",
|
|
881
|
+
)
|
|
882
|
+
params["ai_data_platform_id"] = global_values["ai_data_platform_id"]
|
|
883
|
+
|
|
884
|
+
values = global_values.copy()
|
|
885
|
+
values.update(
|
|
886
|
+
{
|
|
887
|
+
"params": params,
|
|
888
|
+
"body": body,
|
|
889
|
+
"opc_request_id": opc_request_id,
|
|
890
|
+
"no_request_id": no_request_id,
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
return SimpleNamespace(**values)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def command_usage_error(group: CommandGroup, command: CommandDefinition, message: str) -> CliError:
|
|
897
|
+
return CliError(
|
|
898
|
+
f'{message}\n\nUse "aidp {group.name} {command.name} -h" '
|
|
899
|
+
f'or "aidp {group.name} {command.name} --help" for command help.'
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def with_command_usage_hint(group: CommandGroup, command: CommandDefinition, error: CliError) -> CliError:
|
|
904
|
+
message = str(error)
|
|
905
|
+
if "\n\nUse " in message:
|
|
906
|
+
return error
|
|
907
|
+
return command_usage_error(group, command, message)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def command_option_value(
|
|
911
|
+
tokens: list[str],
|
|
912
|
+
index: int,
|
|
913
|
+
option_name: str,
|
|
914
|
+
inline_value: str | None,
|
|
915
|
+
) -> tuple[str, int]:
|
|
916
|
+
if inline_value is not None:
|
|
917
|
+
return inline_value, index + 1
|
|
918
|
+
if index + 1 >= len(tokens):
|
|
919
|
+
raise CliError(f"{option_name} requires a value.")
|
|
920
|
+
return tokens[index + 1], index + 2
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def field_option_value(
|
|
924
|
+
tokens: list[str],
|
|
925
|
+
index: int,
|
|
926
|
+
option_name: str,
|
|
927
|
+
inline_value: str | None,
|
|
928
|
+
field: CommandField,
|
|
929
|
+
) -> tuple[Any, int]:
|
|
930
|
+
if inline_value is not None:
|
|
931
|
+
return inline_value, index + 1
|
|
932
|
+
if field.type_name in {"boolean", "bool"}:
|
|
933
|
+
return True, index + 1
|
|
934
|
+
return command_option_value(tokens, index, option_name, inline_value)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def bind_command_args(
|
|
938
|
+
sdk_method: Callable[..., Any],
|
|
939
|
+
params: dict[str, Any],
|
|
940
|
+
body: Any,
|
|
941
|
+
body_param_name: str | None,
|
|
942
|
+
) -> tuple[list[Any], dict[str, Any]]:
|
|
943
|
+
signature = inspect.signature(sdk_method)
|
|
944
|
+
positional: list[Any] = []
|
|
945
|
+
kwargs: dict[str, Any] = {}
|
|
946
|
+
body_consumed = False
|
|
947
|
+
|
|
948
|
+
for parameter in signature.parameters.values():
|
|
949
|
+
if parameter.name == "self":
|
|
950
|
+
continue
|
|
951
|
+
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
952
|
+
kwargs.update(params)
|
|
953
|
+
params = {}
|
|
954
|
+
continue
|
|
955
|
+
|
|
956
|
+
value_present = parameter.name in params
|
|
957
|
+
if value_present:
|
|
958
|
+
value = params.pop(parameter.name)
|
|
959
|
+
elif body is not None and not body_consumed and parameter.name == body_param_name:
|
|
960
|
+
value = body
|
|
961
|
+
body_consumed = True
|
|
962
|
+
elif parameter.default is inspect.Parameter.empty:
|
|
963
|
+
raise CliError(f"Missing required parameter {parameter_option_name(parameter.name, body_param_name)}.")
|
|
964
|
+
else:
|
|
965
|
+
continue
|
|
966
|
+
|
|
967
|
+
if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
968
|
+
positional.append(value)
|
|
969
|
+
else:
|
|
970
|
+
kwargs[parameter.name] = value
|
|
971
|
+
|
|
972
|
+
if body is not None and not body_consumed:
|
|
973
|
+
raise CliError("A request body was provided, but this command has no body parameter.")
|
|
974
|
+
if params:
|
|
975
|
+
kwargs.update(params)
|
|
976
|
+
return positional, kwargs
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def parameter_option_name(parameter_name: str, body_param_name: str | None) -> str:
|
|
980
|
+
if parameter_name == body_param_name:
|
|
981
|
+
return "--body"
|
|
982
|
+
return f"--{parameter_name.replace('_', '-')}"
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def add_request_id(
|
|
986
|
+
sdk_method: Callable[..., Any],
|
|
987
|
+
command_name: str,
|
|
988
|
+
call_kwargs: dict[str, Any],
|
|
989
|
+
args: SimpleNamespace,
|
|
990
|
+
) -> None:
|
|
991
|
+
if args.opc_request_id and not sdk_method_accepts_kwarg(sdk_method, "opc_request_id"):
|
|
992
|
+
raise CliError(f"{command_name} does not accept opc_request_id.")
|
|
993
|
+
if (
|
|
994
|
+
not args.no_request_id
|
|
995
|
+
and "opc_request_id" not in call_kwargs
|
|
996
|
+
and sdk_method_accepts_kwarg(sdk_method, "opc_request_id")
|
|
997
|
+
):
|
|
998
|
+
call_kwargs["opc_request_id"] = args.opc_request_id or f"aidp-cli-{uuid.uuid4()}"
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def sdk_method_accepts_kwarg(sdk_method: Callable[..., Any], name: str) -> bool:
|
|
1002
|
+
try:
|
|
1003
|
+
source = inspect.getsource(sdk_method)
|
|
1004
|
+
except OSError:
|
|
1005
|
+
return False
|
|
1006
|
+
return f'"{name}"' in source or f"'{name}'" in source
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def parse_value(value: str) -> Any:
|
|
1010
|
+
stripped = value.strip()
|
|
1011
|
+
if stripped == "":
|
|
1012
|
+
return ""
|
|
1013
|
+
try:
|
|
1014
|
+
return json.loads(stripped)
|
|
1015
|
+
except json.JSONDecodeError:
|
|
1016
|
+
return value
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def load_body_input(command: CommandDefinition, value: str | None) -> Any:
|
|
1020
|
+
if command_uses_raw_body(command):
|
|
1021
|
+
return read_raw_body_argument(value)
|
|
1022
|
+
return load_json_input(value)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def command_uses_raw_body(command: CommandDefinition) -> bool:
|
|
1026
|
+
return (
|
|
1027
|
+
command.body_field is not None
|
|
1028
|
+
and not command.body_model
|
|
1029
|
+
and not command.body_fields
|
|
1030
|
+
and not command.body_models
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def load_json_input(value: str | None) -> Any:
|
|
1035
|
+
if value is None:
|
|
1036
|
+
return None
|
|
1037
|
+
raw = read_json_argument(value)
|
|
1038
|
+
try:
|
|
1039
|
+
parsed = json.loads(raw)
|
|
1040
|
+
except json.JSONDecodeError as exc:
|
|
1041
|
+
raise CliError(f"--body is not valid JSON: {exc}") from exc
|
|
1042
|
+
if not isinstance(parsed, (dict, list)):
|
|
1043
|
+
raise CliError("--body must contain a JSON object or array.")
|
|
1044
|
+
return parsed
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def read_raw_body_argument(value: str | None) -> bytes | str | None:
|
|
1048
|
+
if value is None:
|
|
1049
|
+
return None
|
|
1050
|
+
if value == "-":
|
|
1051
|
+
stdin_buffer = getattr(sys.stdin, "buffer", None)
|
|
1052
|
+
if stdin_buffer is not None:
|
|
1053
|
+
return stdin_buffer.read()
|
|
1054
|
+
return sys.stdin.read()
|
|
1055
|
+
if value.startswith("@"):
|
|
1056
|
+
return Path(value[1:]).expanduser().read_bytes()
|
|
1057
|
+
if value.startswith("file://"):
|
|
1058
|
+
return Path(value.removeprefix("file://")).expanduser().read_bytes()
|
|
1059
|
+
return value
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def read_json_argument(value: str) -> str:
|
|
1063
|
+
if value == "-":
|
|
1064
|
+
return sys.stdin.read()
|
|
1065
|
+
if value.startswith("@"):
|
|
1066
|
+
return Path(value[1:]).expanduser().read_text(encoding="utf-8")
|
|
1067
|
+
if value.startswith("file://"):
|
|
1068
|
+
return Path(value.removeprefix("file://")).expanduser().read_text(encoding="utf-8")
|
|
1069
|
+
return value
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def handle_search(args: list[str]) -> None:
|
|
1073
|
+
query = " ".join(args).strip()
|
|
1074
|
+
if not query:
|
|
1075
|
+
print_search_help()
|
|
1076
|
+
return
|
|
1077
|
+
results: list[tuple[int, str, str, str]] = []
|
|
1078
|
+
for group in command_manifest().command_groups:
|
|
1079
|
+
group_score = search_score(query, [group.name, group.description], [], group.name)
|
|
1080
|
+
if group_score:
|
|
1081
|
+
results.append((group_score, group.name, "", group.description))
|
|
1082
|
+
for command in group.commands:
|
|
1083
|
+
fields = " ".join(field.cli_name for field in command.fields)
|
|
1084
|
+
command_score = search_score(
|
|
1085
|
+
query,
|
|
1086
|
+
[group.name, command.name],
|
|
1087
|
+
[command.summary, command.description, fields],
|
|
1088
|
+
f"{group.name} {command.name}",
|
|
1089
|
+
)
|
|
1090
|
+
if command_score:
|
|
1091
|
+
results.append((command_score, group.name, command.name, command_summary(command)))
|
|
1092
|
+
|
|
1093
|
+
if not results:
|
|
1094
|
+
print(f"No results for {query!r}.")
|
|
1095
|
+
return
|
|
1096
|
+
results = filter_search_results(results)
|
|
1097
|
+
print(f"Search results for {query!r}:")
|
|
1098
|
+
rows = []
|
|
1099
|
+
for _score, group_name, command_name, description in sorted(results, key=search_result_sort_key)[:30]:
|
|
1100
|
+
name = f"{group_name} {command_name}".strip()
|
|
1101
|
+
rows.append((name, description))
|
|
1102
|
+
print("\n".join(format_table(rows)))
|
|
1103
|
+
if len(results) > 30:
|
|
1104
|
+
print(f"\nShowing 30 of {len(results)} results.")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def filter_search_results(results: list[tuple[int, str, str, str]]) -> list[tuple[int, str, str, str]]:
|
|
1108
|
+
top_score = max(score for score, _group_name, _command_name, _description in results)
|
|
1109
|
+
if top_score < 850:
|
|
1110
|
+
return results
|
|
1111
|
+
cutoff = int(top_score * 0.5)
|
|
1112
|
+
return [
|
|
1113
|
+
result
|
|
1114
|
+
for result in results
|
|
1115
|
+
if result[0] >= cutoff
|
|
1116
|
+
]
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def search_result_sort_key(result: tuple[int, str, str, str]) -> tuple[int, str, str]:
|
|
1120
|
+
score, group_name, command_name, _description = result
|
|
1121
|
+
return (-score, group_name, command_name)
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def search_score(
|
|
1125
|
+
query: str,
|
|
1126
|
+
primary_values: list[str],
|
|
1127
|
+
secondary_values: list[str],
|
|
1128
|
+
sort_value: str,
|
|
1129
|
+
) -> int:
|
|
1130
|
+
query_tokens = search_tokens(query)
|
|
1131
|
+
if not query_tokens:
|
|
1132
|
+
return 0
|
|
1133
|
+
|
|
1134
|
+
primary_value = " ".join(item for item in primary_values if item)
|
|
1135
|
+
secondary_value = " ".join(item for item in secondary_values if item)
|
|
1136
|
+
primary_tokens = search_tokens(primary_value)
|
|
1137
|
+
secondary_tokens = search_tokens(secondary_value)
|
|
1138
|
+
if not primary_tokens and not secondary_tokens:
|
|
1139
|
+
return 0
|
|
1140
|
+
|
|
1141
|
+
compact_query = compact_search_text(query)
|
|
1142
|
+
compact_primary = compact_search_text(primary_value)
|
|
1143
|
+
compact_secondary = compact_search_text(secondary_value)
|
|
1144
|
+
compact_sort = compact_search_text(sort_value)
|
|
1145
|
+
compact_sort_variants = compact_sort_value_variants(sort_value)
|
|
1146
|
+
expanded_primary_tokens = expand_search_tokens(primary_tokens)
|
|
1147
|
+
expanded_secondary_tokens = expand_search_tokens(secondary_tokens)
|
|
1148
|
+
|
|
1149
|
+
if is_compact_action_query(query_tokens, compact_query):
|
|
1150
|
+
return max(
|
|
1151
|
+
compact_identity_search_score(compact_query, sort_variant)
|
|
1152
|
+
for sort_variant in compact_sort_variants
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
score = 0
|
|
1156
|
+
matched = False
|
|
1157
|
+
|
|
1158
|
+
sort_identity_score = max(
|
|
1159
|
+
compact_identity_search_score(compact_query, sort_variant)
|
|
1160
|
+
for sort_variant in compact_sort_variants
|
|
1161
|
+
)
|
|
1162
|
+
if sort_identity_score >= 850:
|
|
1163
|
+
score += sort_identity_score
|
|
1164
|
+
matched = True
|
|
1165
|
+
elif compact_query == compact_sort:
|
|
1166
|
+
score += 1000
|
|
1167
|
+
matched = True
|
|
1168
|
+
elif compact_sort.startswith(compact_query):
|
|
1169
|
+
score += 900
|
|
1170
|
+
matched = True
|
|
1171
|
+
elif compact_query in compact_sort:
|
|
1172
|
+
score += 850
|
|
1173
|
+
matched = True
|
|
1174
|
+
elif compact_query in compact_primary:
|
|
1175
|
+
score += 700
|
|
1176
|
+
matched = True
|
|
1177
|
+
elif compact_query in compact_secondary:
|
|
1178
|
+
score += 350
|
|
1179
|
+
matched = True
|
|
1180
|
+
|
|
1181
|
+
if all(query_token_matches(token, expanded_primary_tokens) for token in query_tokens):
|
|
1182
|
+
score += 500
|
|
1183
|
+
matched = True
|
|
1184
|
+
if search_tokens_in_order(query_tokens, primary_tokens):
|
|
1185
|
+
score += 150
|
|
1186
|
+
elif len(query_tokens) == 1 and query_token_matches(query_tokens[0], expanded_secondary_tokens):
|
|
1187
|
+
score += 150
|
|
1188
|
+
matched = True
|
|
1189
|
+
|
|
1190
|
+
for token in query_tokens:
|
|
1191
|
+
if token in expanded_primary_tokens:
|
|
1192
|
+
score += 25
|
|
1193
|
+
elif len(query_tokens) == 1 and token in expanded_secondary_tokens:
|
|
1194
|
+
score += 10
|
|
1195
|
+
|
|
1196
|
+
fuzzy_ratio = max(
|
|
1197
|
+
difflib.SequenceMatcher(None, compact_query, sort_variant).ratio()
|
|
1198
|
+
for sort_variant in compact_sort_variants
|
|
1199
|
+
)
|
|
1200
|
+
if fuzzy_ratio >= 0.72 and compact_length_ratio(compact_query, compact_sort) >= 0.8:
|
|
1201
|
+
score += int(fuzzy_ratio * 250)
|
|
1202
|
+
matched = True
|
|
1203
|
+
|
|
1204
|
+
return score if matched else 0
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def is_compact_action_query(query_tokens: list[str], compact_query: str) -> bool:
|
|
1208
|
+
if len(query_tokens) != 1:
|
|
1209
|
+
return False
|
|
1210
|
+
return any(
|
|
1211
|
+
compact_query.startswith(action) and len(compact_query) > len(action) + 2
|
|
1212
|
+
for action in SEARCH_ACTION_PREFIXES
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def compact_identity_search_score(compact_query: str, compact_sort: str) -> int:
|
|
1217
|
+
for query_variant in compact_search_variants(compact_query):
|
|
1218
|
+
if query_variant == compact_sort:
|
|
1219
|
+
return 1000
|
|
1220
|
+
if compact_sort.startswith(query_variant):
|
|
1221
|
+
return 900
|
|
1222
|
+
if query_variant in compact_sort:
|
|
1223
|
+
return 850
|
|
1224
|
+
return 0
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def compact_search_variants(value: str) -> tuple[str, ...]:
|
|
1228
|
+
variants = [value]
|
|
1229
|
+
if len(value) > 4 and value.endswith("ies"):
|
|
1230
|
+
variants.append(value[:-3] + "y")
|
|
1231
|
+
if len(value) > 3 and value.endswith("s"):
|
|
1232
|
+
variants.append(value[:-1])
|
|
1233
|
+
return tuple(dict.fromkeys(variants))
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
def compact_sort_value_variants(value: str) -> tuple[str, ...]:
|
|
1237
|
+
tokens = search_tokens(value)
|
|
1238
|
+
variants = [compact_search_text(value)]
|
|
1239
|
+
if len(tokens) >= 2 and tokens[-1] in SEARCH_ACTION_PREFIXES:
|
|
1240
|
+
variants.append("".join([tokens[-1], *tokens[:-1]]))
|
|
1241
|
+
return tuple(dict.fromkeys(variants))
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def compact_length_ratio(left: str, right: str) -> float:
|
|
1245
|
+
if not left or not right:
|
|
1246
|
+
return 0
|
|
1247
|
+
return min(len(left), len(right)) / max(len(left), len(right))
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def search_tokens(value: str) -> list[str]:
|
|
1251
|
+
return re.findall(r"[a-z0-9]+", value.lower())
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def compact_search_text(value: str) -> str:
|
|
1255
|
+
return "".join(search_tokens(value))
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def expand_search_tokens(tokens: list[str]) -> set[str]:
|
|
1259
|
+
expanded: set[str] = set()
|
|
1260
|
+
for token in tokens:
|
|
1261
|
+
expanded.update(search_token_variants(token))
|
|
1262
|
+
return expanded
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def search_token_variants(token: str) -> set[str]:
|
|
1266
|
+
variants = {token}
|
|
1267
|
+
if len(token) > 4 and token.endswith("ies"):
|
|
1268
|
+
variants.add(token[:-3] + "y")
|
|
1269
|
+
if len(token) > 3 and token.endswith("s"):
|
|
1270
|
+
variants.add(token[:-1])
|
|
1271
|
+
return variants
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def query_token_matches(query_token: str, value_tokens: set[str]) -> bool:
|
|
1275
|
+
if search_token_variants(query_token) & value_tokens:
|
|
1276
|
+
return True
|
|
1277
|
+
if len(query_token) < 5:
|
|
1278
|
+
return False
|
|
1279
|
+
return bool(difflib.get_close_matches(query_token, value_tokens, n=1, cutoff=0.82))
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def search_tokens_in_order(query_tokens: list[str], value_tokens: list[str]) -> bool:
|
|
1283
|
+
value_index = 0
|
|
1284
|
+
expanded_values = [search_token_variants(token) for token in value_tokens]
|
|
1285
|
+
for query_token in query_tokens:
|
|
1286
|
+
query_variants = search_token_variants(query_token)
|
|
1287
|
+
while value_index < len(expanded_values):
|
|
1288
|
+
if query_variants & expanded_values[value_index]:
|
|
1289
|
+
value_index += 1
|
|
1290
|
+
break
|
|
1291
|
+
value_index += 1
|
|
1292
|
+
else:
|
|
1293
|
+
return False
|
|
1294
|
+
return True
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def print_search_help() -> None:
|
|
1298
|
+
lines = [
|
|
1299
|
+
"Search AIDP command groups, command names, descriptions, and flags.",
|
|
1300
|
+
"",
|
|
1301
|
+
"Usage:",
|
|
1302
|
+
" aidp search QUERY [flags]",
|
|
1303
|
+
"",
|
|
1304
|
+
"Examples:",
|
|
1305
|
+
" aidp search workspace",
|
|
1306
|
+
" aidp search list workspaces",
|
|
1307
|
+
" aidp search permission",
|
|
1308
|
+
"",
|
|
1309
|
+
"Flags:",
|
|
1310
|
+
*format_flags([("-h, --help", "help for search")]),
|
|
1311
|
+
"",
|
|
1312
|
+
"Global Flags:",
|
|
1313
|
+
*format_flags(global_flag_rows()),
|
|
1314
|
+
]
|
|
1315
|
+
print("\n".join(lines))
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def handle_configure(args: list[str]) -> None:
|
|
1319
|
+
action = args[0]
|
|
1320
|
+
if action == "get":
|
|
1321
|
+
print("AIDP CLI configuration:")
|
|
1322
|
+
print(f" instance-id: {os.getenv('AIDP_INSTANCE_ID') or os.getenv('INSTANCE_ID') or '(not set)'}")
|
|
1323
|
+
print(f" endpoint: {os.getenv('AIDP_ENDPOINT') or os.getenv('OCI_CLI_ENDPOINT') or '(not set)'}")
|
|
1324
|
+
return
|
|
1325
|
+
if action == "set":
|
|
1326
|
+
raise CliError("aidp configure set is no longer supported. Use AIDP_INSTANCE_ID or --instance-id.")
|
|
1327
|
+
raise CliError(f"Unknown configure command {action!r}. Run 'aidp configure --help'.")
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def print_configure_help() -> None:
|
|
1331
|
+
lines = [
|
|
1332
|
+
"Show AIDP CLI environment defaults.",
|
|
1333
|
+
"",
|
|
1334
|
+
"Usage:",
|
|
1335
|
+
" aidp configure get",
|
|
1336
|
+
"",
|
|
1337
|
+
"Set defaults with AIDP_INSTANCE_ID and AIDP_ENDPOINT environment variables.",
|
|
1338
|
+
"",
|
|
1339
|
+
"Flags:",
|
|
1340
|
+
*format_flags([("-h, --help", "help for configure")]),
|
|
1341
|
+
]
|
|
1342
|
+
print("\n".join(lines))
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def configured_instance_id() -> str | None:
|
|
1346
|
+
env_value = os.getenv("AIDP_INSTANCE_ID") or os.getenv("INSTANCE_ID")
|
|
1347
|
+
if env_value:
|
|
1348
|
+
return env_value
|
|
1349
|
+
return None
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def unknown_command_group(name: str) -> CliError:
|
|
1353
|
+
candidates = ["command-groups", "search", "configure", "help", "version"] + [
|
|
1354
|
+
group.name for group in command_manifest().command_groups
|
|
1355
|
+
]
|
|
1356
|
+
return CliError(unknown_message("command group", name, candidates, "aidp command-groups"))
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def unknown_command_name(group: CommandGroup, name: str) -> CliError:
|
|
1360
|
+
candidates = [command.name for command in group.commands]
|
|
1361
|
+
matches = command_name_suggestions(group, name, candidates)
|
|
1362
|
+
return CliError(unknown_message("command name", name, candidates, f"aidp {group.name} --help", matches))
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def command_name_suggestions(group: CommandGroup, name: str, candidates: list[str]) -> list[str]:
|
|
1366
|
+
matches = difflib.get_close_matches(name, candidates, n=3)
|
|
1367
|
+
if matches:
|
|
1368
|
+
return matches
|
|
1369
|
+
|
|
1370
|
+
normalized = compact_search_text(name)
|
|
1371
|
+
alias_matches = []
|
|
1372
|
+
for command in group.commands:
|
|
1373
|
+
aliases = {
|
|
1374
|
+
command.sdk_method_name.replace("_", "-"),
|
|
1375
|
+
command.operation_id,
|
|
1376
|
+
}
|
|
1377
|
+
if any(normalized == compact_search_text(alias) for alias in aliases):
|
|
1378
|
+
alias_matches.append(command.name)
|
|
1379
|
+
if alias_matches:
|
|
1380
|
+
return list(dict.fromkeys(alias_matches))[:3]
|
|
1381
|
+
|
|
1382
|
+
scored = [
|
|
1383
|
+
(search_score(name, [group.name, command.name, command.sdk_method_name], [command.summary], command.name), command.name)
|
|
1384
|
+
for command in group.commands
|
|
1385
|
+
]
|
|
1386
|
+
best_score = max((score for score, _command_name in scored), default=0)
|
|
1387
|
+
if best_score < 500:
|
|
1388
|
+
return []
|
|
1389
|
+
cutoff = max(500, int(best_score * 0.75))
|
|
1390
|
+
return [
|
|
1391
|
+
command_name
|
|
1392
|
+
for score, command_name in sorted(scored, key=lambda item: (-item[0], item[1]))
|
|
1393
|
+
if score >= cutoff
|
|
1394
|
+
][:3]
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def unknown_option(
|
|
1398
|
+
group: CommandGroup,
|
|
1399
|
+
command: CommandDefinition,
|
|
1400
|
+
name: str,
|
|
1401
|
+
option_fields: dict[str, CommandField],
|
|
1402
|
+
) -> CliError:
|
|
1403
|
+
candidates = (
|
|
1404
|
+
["--body", "--opc-request-id", "--no-request-id"]
|
|
1405
|
+
+ list(option_fields)
|
|
1406
|
+
+ list(GLOBAL_VALUE_OPTIONS)
|
|
1407
|
+
+ list(GLOBAL_BOOLEAN_OPTIONS)
|
|
1408
|
+
)
|
|
1409
|
+
return CliError(
|
|
1410
|
+
unknown_message("option", name, candidates, f"aidp help {group.name} {command.name}")
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def unknown_message(
|
|
1415
|
+
kind: str,
|
|
1416
|
+
name: str,
|
|
1417
|
+
candidates: list[str],
|
|
1418
|
+
help_command: str,
|
|
1419
|
+
matches: list[str] | None = None,
|
|
1420
|
+
) -> str:
|
|
1421
|
+
message = f"Unknown {kind} {name!r}."
|
|
1422
|
+
matches = matches if matches is not None else difflib.get_close_matches(name, candidates, n=3)
|
|
1423
|
+
if matches:
|
|
1424
|
+
message += "\n\nDid you mean this?\n"
|
|
1425
|
+
message += "\n".join(f" {match}" for match in matches)
|
|
1426
|
+
message += f'\n\nUse "{help_command}" for more information.'
|
|
1427
|
+
return message
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def print_version() -> None:
|
|
1431
|
+
print(f"aidp python cli version-{cli_version()}")
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def cli_version() -> str:
|
|
1435
|
+
try:
|
|
1436
|
+
return package_version(PACKAGE_NAME)
|
|
1437
|
+
except PackageNotFoundError:
|
|
1438
|
+
return "unknown"
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def example_body(command: CommandDefinition) -> dict[str, Any] | None:
|
|
1442
|
+
if command.body_field is None:
|
|
1443
|
+
return None
|
|
1444
|
+
model = root_body_model(command)
|
|
1445
|
+
fields = model.fields if model is not None else command.body_fields
|
|
1446
|
+
return sample_object_for_fields(fields, command, seen_models=set(), depth=0)
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def root_body_variant_examples(command: CommandDefinition) -> list[tuple[str, dict[str, Any]]]:
|
|
1450
|
+
root_model = root_body_model(command)
|
|
1451
|
+
if root_model is None or not root_model.variants:
|
|
1452
|
+
return []
|
|
1453
|
+
examples = []
|
|
1454
|
+
for variant in root_model.variants:
|
|
1455
|
+
if variant.model_name not in command.body_models:
|
|
1456
|
+
continue
|
|
1457
|
+
label = (
|
|
1458
|
+
f"{variant.model_name} "
|
|
1459
|
+
f"({variant.discriminator_field}={variant.discriminator_value})"
|
|
1460
|
+
)
|
|
1461
|
+
examples.append((label, variant_body_sample(command, command.body_model, variant)))
|
|
1462
|
+
return examples
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def nested_body_variant_examples(command: CommandDefinition) -> list[tuple[str, str, dict[str, Any]]]:
|
|
1466
|
+
root_model = root_body_model(command)
|
|
1467
|
+
if root_model is None:
|
|
1468
|
+
return []
|
|
1469
|
+
examples = collect_nested_body_variant_examples(
|
|
1470
|
+
root_model.fields,
|
|
1471
|
+
command,
|
|
1472
|
+
parent_path="",
|
|
1473
|
+
seen_models={command.body_model},
|
|
1474
|
+
depth=0,
|
|
1475
|
+
)
|
|
1476
|
+
for variant in root_model.variants:
|
|
1477
|
+
variant_model = command.body_models.get(variant.model_name)
|
|
1478
|
+
if variant_model is None:
|
|
1479
|
+
continue
|
|
1480
|
+
examples.extend(
|
|
1481
|
+
collect_nested_body_variant_examples(
|
|
1482
|
+
variant_model.fields,
|
|
1483
|
+
command,
|
|
1484
|
+
parent_path="",
|
|
1485
|
+
seen_models={command.body_model, variant.model_name},
|
|
1486
|
+
depth=0,
|
|
1487
|
+
)
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
unique_examples = []
|
|
1491
|
+
seen = set()
|
|
1492
|
+
for path, label, sample in examples:
|
|
1493
|
+
key = (path, label)
|
|
1494
|
+
if key in seen:
|
|
1495
|
+
continue
|
|
1496
|
+
seen.add(key)
|
|
1497
|
+
unique_examples.append((path, label, sample))
|
|
1498
|
+
return unique_examples
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def collect_nested_body_variant_examples(
|
|
1502
|
+
fields: tuple[BodyField, ...],
|
|
1503
|
+
command: CommandDefinition,
|
|
1504
|
+
parent_path: str,
|
|
1505
|
+
seen_models: set[str],
|
|
1506
|
+
depth: int,
|
|
1507
|
+
) -> list[tuple[str, str, dict[str, Any]]]:
|
|
1508
|
+
if depth >= MAX_BODY_EXAMPLE_DEPTH:
|
|
1509
|
+
return []
|
|
1510
|
+
|
|
1511
|
+
examples = []
|
|
1512
|
+
for field in fields:
|
|
1513
|
+
if not field.model_name:
|
|
1514
|
+
continue
|
|
1515
|
+
model = command.body_models.get(field.model_name)
|
|
1516
|
+
if model is None:
|
|
1517
|
+
continue
|
|
1518
|
+
|
|
1519
|
+
path = f"{parent_path}.{field.name}" if parent_path else field.name
|
|
1520
|
+
model_path = f"{path}[]" if field.type_name == "array" else path
|
|
1521
|
+
for variant in model.variants:
|
|
1522
|
+
if variant.model_name not in command.body_models:
|
|
1523
|
+
continue
|
|
1524
|
+
label = (
|
|
1525
|
+
f"{variant.model_name} "
|
|
1526
|
+
f"({variant.discriminator_field}={variant.discriminator_value})"
|
|
1527
|
+
)
|
|
1528
|
+
examples.append((model_path, label, variant_body_sample(command, field.model_name, variant)))
|
|
1529
|
+
|
|
1530
|
+
if field.model_name not in seen_models:
|
|
1531
|
+
next_seen_models = set(seen_models)
|
|
1532
|
+
next_seen_models.add(field.model_name)
|
|
1533
|
+
examples.extend(
|
|
1534
|
+
collect_nested_body_variant_examples(
|
|
1535
|
+
model.fields,
|
|
1536
|
+
command,
|
|
1537
|
+
parent_path=model_path,
|
|
1538
|
+
seen_models=next_seen_models,
|
|
1539
|
+
depth=depth + 1,
|
|
1540
|
+
)
|
|
1541
|
+
)
|
|
1542
|
+
return examples
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def variant_body_sample(
|
|
1546
|
+
command: CommandDefinition,
|
|
1547
|
+
base_model_name: str,
|
|
1548
|
+
variant: Any,
|
|
1549
|
+
) -> dict[str, Any]:
|
|
1550
|
+
base_model = command.body_models[base_model_name]
|
|
1551
|
+
variant_model = command.body_models[variant.model_name]
|
|
1552
|
+
sample = sample_object_for_fields(base_model.fields, command, seen_models=set(), depth=0)
|
|
1553
|
+
variant_sample = sample_object_for_fields(
|
|
1554
|
+
variant_model.fields,
|
|
1555
|
+
command,
|
|
1556
|
+
seen_models={base_model_name},
|
|
1557
|
+
depth=0,
|
|
1558
|
+
)
|
|
1559
|
+
for name, value in variant_sample.items():
|
|
1560
|
+
if name not in sample:
|
|
1561
|
+
sample[name] = value
|
|
1562
|
+
sample[variant.discriminator_field] = variant.discriminator_value
|
|
1563
|
+
return sample
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def sample_object_for_fields(
|
|
1567
|
+
fields: tuple[BodyField, ...],
|
|
1568
|
+
command: CommandDefinition,
|
|
1569
|
+
seen_models: set[str],
|
|
1570
|
+
depth: int,
|
|
1571
|
+
) -> dict[str, Any]:
|
|
1572
|
+
return {
|
|
1573
|
+
field.name: sample_body_value(field, command, seen_models=seen_models, depth=depth)
|
|
1574
|
+
for field in fields
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
def root_body_fields(command: CommandDefinition) -> tuple[BodyField, ...]:
|
|
1579
|
+
model = root_body_model(command)
|
|
1580
|
+
return model.fields if model is not None else command.body_fields
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def root_body_model(command: CommandDefinition) -> BodyModel | None:
|
|
1584
|
+
if command.body_model and command.body_model in command.body_models:
|
|
1585
|
+
return command.body_models[command.body_model]
|
|
1586
|
+
return None
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def sample_body_value(
|
|
1590
|
+
field: BodyField,
|
|
1591
|
+
command: CommandDefinition,
|
|
1592
|
+
seen_models: set[str],
|
|
1593
|
+
depth: int,
|
|
1594
|
+
) -> Any:
|
|
1595
|
+
if field.type_name == "array":
|
|
1596
|
+
return [sample_array_item(field, command, seen_models, depth)]
|
|
1597
|
+
if field.model_name:
|
|
1598
|
+
return sample_model_value(field.model_name, command, seen_models, depth)
|
|
1599
|
+
return sample_scalar_value(field.type_name, field.enum_values)
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
def sample_array_item(
|
|
1603
|
+
field: BodyField,
|
|
1604
|
+
command: CommandDefinition,
|
|
1605
|
+
seen_models: set[str],
|
|
1606
|
+
depth: int,
|
|
1607
|
+
) -> Any:
|
|
1608
|
+
if field.model_name:
|
|
1609
|
+
return sample_model_value(field.model_name, command, seen_models, depth)
|
|
1610
|
+
return sample_scalar_value(field.item_type or "string", field.enum_values)
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def sample_model_value(
|
|
1614
|
+
model_name: str,
|
|
1615
|
+
command: CommandDefinition,
|
|
1616
|
+
seen_models: set[str],
|
|
1617
|
+
depth: int,
|
|
1618
|
+
) -> dict[str, Any]:
|
|
1619
|
+
if depth >= MAX_BODY_EXAMPLE_DEPTH or model_name in seen_models:
|
|
1620
|
+
return {}
|
|
1621
|
+
model = command.body_models.get(model_name)
|
|
1622
|
+
if model is None:
|
|
1623
|
+
return {}
|
|
1624
|
+
next_seen_models = set(seen_models)
|
|
1625
|
+
next_seen_models.add(model_name)
|
|
1626
|
+
return {
|
|
1627
|
+
field.name: sample_body_value(field, command, next_seen_models, depth + 1)
|
|
1628
|
+
for field in model.fields
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def sample_scalar_value(type_name: str, enum_values: tuple[Any, ...]) -> Any:
|
|
1633
|
+
if enum_values:
|
|
1634
|
+
return enum_values[0]
|
|
1635
|
+
if type_name in {"boolean", "bool"}:
|
|
1636
|
+
return False
|
|
1637
|
+
if type_name in {"integer", "number", "int", "float"}:
|
|
1638
|
+
return 0
|
|
1639
|
+
if type_name == "array":
|
|
1640
|
+
return []
|
|
1641
|
+
if type_name == "object":
|
|
1642
|
+
return {}
|
|
1643
|
+
return "<string>"
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def body_required_fields(command: CommandDefinition) -> tuple[str, ...]:
|
|
1647
|
+
if not command.body_models:
|
|
1648
|
+
return command.body_required_fields
|
|
1649
|
+
return tuple(
|
|
1650
|
+
collect_required_body_paths(
|
|
1651
|
+
root_body_fields(command),
|
|
1652
|
+
command,
|
|
1653
|
+
parent_path="",
|
|
1654
|
+
parent_required=True,
|
|
1655
|
+
seen_models=set(),
|
|
1656
|
+
depth=0,
|
|
1657
|
+
)
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def collect_required_body_paths(
|
|
1662
|
+
fields: tuple[BodyField, ...],
|
|
1663
|
+
command: CommandDefinition,
|
|
1664
|
+
parent_path: str,
|
|
1665
|
+
parent_required: bool,
|
|
1666
|
+
seen_models: set[str],
|
|
1667
|
+
depth: int,
|
|
1668
|
+
) -> list[str]:
|
|
1669
|
+
if depth >= MAX_BODY_EXAMPLE_DEPTH:
|
|
1670
|
+
return []
|
|
1671
|
+
|
|
1672
|
+
required_paths = []
|
|
1673
|
+
for field in fields:
|
|
1674
|
+
path = f"{parent_path}.{field.name}" if parent_path else field.name
|
|
1675
|
+
is_unconditionally_required = parent_required and field.required
|
|
1676
|
+
if is_unconditionally_required:
|
|
1677
|
+
required_paths.append(path)
|
|
1678
|
+
|
|
1679
|
+
nested_path = f"{path}[]" if field.type_name == "array" else path
|
|
1680
|
+
if field.model_name and field.model_name not in seen_models:
|
|
1681
|
+
model = command.body_models.get(field.model_name)
|
|
1682
|
+
if model is None:
|
|
1683
|
+
continue
|
|
1684
|
+
next_seen_models = set(seen_models)
|
|
1685
|
+
next_seen_models.add(field.model_name)
|
|
1686
|
+
required_paths.extend(
|
|
1687
|
+
collect_required_body_paths(
|
|
1688
|
+
model.fields,
|
|
1689
|
+
command,
|
|
1690
|
+
nested_path,
|
|
1691
|
+
is_unconditionally_required,
|
|
1692
|
+
next_seen_models,
|
|
1693
|
+
depth + 1,
|
|
1694
|
+
)
|
|
1695
|
+
)
|
|
1696
|
+
return required_paths
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def body_enum_fields(command: CommandDefinition) -> dict[str, tuple[Any, ...]]:
|
|
1700
|
+
if not command.body_models:
|
|
1701
|
+
return command.body_enum_fields
|
|
1702
|
+
return collect_body_enum_paths(
|
|
1703
|
+
root_body_fields(command),
|
|
1704
|
+
command,
|
|
1705
|
+
parent_path="",
|
|
1706
|
+
seen_models=set(),
|
|
1707
|
+
depth=0,
|
|
1708
|
+
)
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
def collect_body_enum_paths(
|
|
1712
|
+
fields: tuple[BodyField, ...],
|
|
1713
|
+
command: CommandDefinition,
|
|
1714
|
+
parent_path: str,
|
|
1715
|
+
seen_models: set[str],
|
|
1716
|
+
depth: int,
|
|
1717
|
+
) -> dict[str, tuple[Any, ...]]:
|
|
1718
|
+
if depth >= MAX_BODY_EXAMPLE_DEPTH:
|
|
1719
|
+
return {}
|
|
1720
|
+
|
|
1721
|
+
enum_paths: dict[str, tuple[Any, ...]] = {}
|
|
1722
|
+
for field in fields:
|
|
1723
|
+
path = f"{parent_path}.{field.name}" if parent_path else field.name
|
|
1724
|
+
if field.enum_values:
|
|
1725
|
+
enum_paths[path] = field.enum_values
|
|
1726
|
+
|
|
1727
|
+
nested_path = f"{path}[]" if field.type_name == "array" else path
|
|
1728
|
+
if field.model_name and field.model_name not in seen_models:
|
|
1729
|
+
model = command.body_models.get(field.model_name)
|
|
1730
|
+
if model is None:
|
|
1731
|
+
continue
|
|
1732
|
+
next_seen_models = set(seen_models)
|
|
1733
|
+
next_seen_models.add(field.model_name)
|
|
1734
|
+
enum_paths.update(
|
|
1735
|
+
collect_body_enum_paths(
|
|
1736
|
+
model.fields,
|
|
1737
|
+
command,
|
|
1738
|
+
nested_path,
|
|
1739
|
+
next_seen_models,
|
|
1740
|
+
depth + 1,
|
|
1741
|
+
)
|
|
1742
|
+
)
|
|
1743
|
+
return enum_paths
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def load_config(args: SimpleNamespace) -> dict[str, Any]:
|
|
1747
|
+
if args.auth in {"instance_principal", "resource_principal"}:
|
|
1748
|
+
return {"region": args.region} if args.region else {}
|
|
1749
|
+
config = oci.config.from_file(
|
|
1750
|
+
file_location=os.path.expanduser(args.config_file),
|
|
1751
|
+
profile_name=args.profile,
|
|
1752
|
+
)
|
|
1753
|
+
apply_config_env_overrides(config)
|
|
1754
|
+
return config
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def build_signer(config: dict[str, Any], args: SimpleNamespace) -> Any:
|
|
1758
|
+
if args.auth == "instance_principal":
|
|
1759
|
+
return oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
|
|
1760
|
+
if args.auth == "resource_principal":
|
|
1761
|
+
return oci.auth.signers.get_resource_principals_signer()
|
|
1762
|
+
if args.auth == "security_token":
|
|
1763
|
+
token_file = expand_path(config.get("security_token_file"))
|
|
1764
|
+
if not token_file:
|
|
1765
|
+
raise CliError("security_token auth requires security_token_file in the OCI config profile.")
|
|
1766
|
+
key_file = expand_path(config.get("key_file"))
|
|
1767
|
+
if not key_file:
|
|
1768
|
+
raise CliError("security_token auth requires key_file in the OCI config profile.")
|
|
1769
|
+
token = Path(token_file).read_text(encoding="utf-8").strip()
|
|
1770
|
+
private_key = load_private_key_from_file(key_file, config.get("pass_phrase"))
|
|
1771
|
+
return SecurityTokenSigner(token=token, private_key=private_key)
|
|
1772
|
+
require_config_keys(config, ["tenancy", "user", "fingerprint", "key_file"], "api_key")
|
|
1773
|
+
return Signer(
|
|
1774
|
+
tenancy=config["tenancy"],
|
|
1775
|
+
user=config["user"],
|
|
1776
|
+
fingerprint=config["fingerprint"],
|
|
1777
|
+
private_key_file_location=expand_path(config.get("key_file")),
|
|
1778
|
+
pass_phrase=config.get("pass_phrase"),
|
|
1779
|
+
private_key_content=config.get("key_content"),
|
|
1780
|
+
)
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def require_config_keys(config: dict[str, Any], keys: list[str], auth: str) -> None:
|
|
1784
|
+
missing_keys = [key for key in keys if not config.get(key)]
|
|
1785
|
+
if not missing_keys:
|
|
1786
|
+
return
|
|
1787
|
+
hint = ""
|
|
1788
|
+
if auth == "api_key" and config.get("security_token_file"):
|
|
1789
|
+
hint = " This profile looks like a security_token profile; retry with --auth security_token."
|
|
1790
|
+
raise CliError(
|
|
1791
|
+
f"{auth} auth requires {', '.join(keys)} in the OCI config profile. "
|
|
1792
|
+
f"Missing: {', '.join(missing_keys)}.{hint}"
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
def apply_config_env_overrides(config: dict[str, Any]) -> None:
|
|
1797
|
+
for env_name, config_key in CONFIG_ENV_OVERRIDES.items():
|
|
1798
|
+
value = os.getenv(env_name)
|
|
1799
|
+
if value:
|
|
1800
|
+
config[config_key] = value
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
def build_client(client_cls: type, args: SimpleNamespace) -> Any:
|
|
1804
|
+
config = load_config(args)
|
|
1805
|
+
signer = build_signer(config, args)
|
|
1806
|
+
region = args.region or getattr(signer, "region", None) or config.get("region")
|
|
1807
|
+
endpoint = resolve_endpoint(
|
|
1808
|
+
region=region,
|
|
1809
|
+
endpoint=args.endpoint,
|
|
1810
|
+
environment_prefix=args.environment_prefix,
|
|
1811
|
+
environment_domain=args.environment_domain,
|
|
1812
|
+
environment_host=args.environment_host,
|
|
1813
|
+
)
|
|
1814
|
+
kwargs: dict[str, Any] = {
|
|
1815
|
+
"signer": signer,
|
|
1816
|
+
"service_endpoint": endpoint,
|
|
1817
|
+
"skip_deserialization": True,
|
|
1818
|
+
}
|
|
1819
|
+
if args.timeout is not None:
|
|
1820
|
+
kwargs["timeout"] = args.timeout
|
|
1821
|
+
client = client_cls(config, **kwargs)
|
|
1822
|
+
apply_client_overrides(client)
|
|
1823
|
+
return client
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def apply_client_overrides(client: Any) -> None:
|
|
1827
|
+
base_client = getattr(client, "base_client", None)
|
|
1828
|
+
if client.__class__.__name__ not in STRICT_URL_ENCODING_CLIENTS:
|
|
1829
|
+
return
|
|
1830
|
+
if base_client is None:
|
|
1831
|
+
return
|
|
1832
|
+
if hasattr(base_client, "enable_strict_url_encoding"):
|
|
1833
|
+
base_client.enable_strict_url_encoding = True
|
|
1834
|
+
else:
|
|
1835
|
+
setattr(base_client, "_enable_strict_url_encoding", True)
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def resolve_endpoint(
|
|
1839
|
+
region: str | None,
|
|
1840
|
+
endpoint: str | None,
|
|
1841
|
+
environment_prefix: str,
|
|
1842
|
+
environment_domain: str,
|
|
1843
|
+
environment_host: str | None,
|
|
1844
|
+
) -> str:
|
|
1845
|
+
if endpoint:
|
|
1846
|
+
return normalize_endpoint(endpoint)
|
|
1847
|
+
if environment_host:
|
|
1848
|
+
return f"https://{environment_host.strip().removeprefix('https://').removeprefix('http://').rstrip('/')}"
|
|
1849
|
+
if not region:
|
|
1850
|
+
raise CliError(
|
|
1851
|
+
"Set --region, --endpoint, --environment-host, or region in the OCI config profile."
|
|
1852
|
+
)
|
|
1853
|
+
if str(region).startswith(("https://", "http://")):
|
|
1854
|
+
raise CliError(
|
|
1855
|
+
"Region must be an OCI region identifier. For a full service URL, use --endpoint, "
|
|
1856
|
+
"AIDP_ENDPOINT, or OCI_CLI_ENDPOINT."
|
|
1857
|
+
)
|
|
1858
|
+
return f"https://{environment_prefix}.{region}.oci.{environment_domain}".rstrip("/")
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def normalize_endpoint(endpoint: str) -> str:
|
|
1862
|
+
stripped = endpoint.strip().rstrip("/")
|
|
1863
|
+
if not stripped:
|
|
1864
|
+
raise CliError("--endpoint cannot be empty.")
|
|
1865
|
+
if stripped.startswith(("https://", "http://")):
|
|
1866
|
+
return stripped
|
|
1867
|
+
return f"https://{stripped}"
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
def expand_path(value: str | None) -> str | None:
|
|
1871
|
+
return os.path.expanduser(value) if value else value
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
def close_client(client: Any) -> None:
|
|
1875
|
+
close = getattr(client, "close", None)
|
|
1876
|
+
if callable(close):
|
|
1877
|
+
close()
|
|
1878
|
+
return
|
|
1879
|
+
base_client = getattr(client, "base_client", None)
|
|
1880
|
+
session = getattr(base_client, "session", None)
|
|
1881
|
+
session_close = getattr(session, "close", None)
|
|
1882
|
+
if callable(session_close):
|
|
1883
|
+
session_close()
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def enable_request_debug(client: Any, command_group: str, command_name: str) -> None:
|
|
1887
|
+
base_client = getattr(client, "base_client", None)
|
|
1888
|
+
if base_client is None:
|
|
1889
|
+
return
|
|
1890
|
+
|
|
1891
|
+
original_call_api = base_client.call_api
|
|
1892
|
+
|
|
1893
|
+
def debug_call_api(*args: Any, **kwargs: Any) -> Any:
|
|
1894
|
+
debug_request(base_client, command_group, command_name, *args, **kwargs)
|
|
1895
|
+
return original_call_api(*args, **kwargs)
|
|
1896
|
+
|
|
1897
|
+
base_client.call_api = debug_call_api
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def debug_request(base_client: Any, command_group: str, command_name: str, *args: Any, **kwargs: Any) -> None:
|
|
1901
|
+
resource_path = get_call_value("resource_path", 0, args, kwargs) or ""
|
|
1902
|
+
method = get_call_value("method", 1, args, kwargs) or ""
|
|
1903
|
+
path_params = get_call_value("path_params", 2, args, kwargs) or {}
|
|
1904
|
+
query_params = get_call_value("query_params", 3, args, kwargs) or {}
|
|
1905
|
+
header_params = get_call_value("header_params", 4, args, kwargs) or {}
|
|
1906
|
+
body = get_call_value("body", 5, args, kwargs)
|
|
1907
|
+
base_path = getattr(base_client, "base_path", "") or ""
|
|
1908
|
+
endpoint = getattr(base_client, "endpoint", "") or ""
|
|
1909
|
+
enable_strict_url_encoding = get_call_value("enable_strict_url_encoding", 9, args, kwargs)
|
|
1910
|
+
rendered_path = render_resource_path(
|
|
1911
|
+
resource_path,
|
|
1912
|
+
path_params,
|
|
1913
|
+
strict_url_encoding=should_render_strict_url_encoding(
|
|
1914
|
+
base_client,
|
|
1915
|
+
enable_strict_url_encoding,
|
|
1916
|
+
),
|
|
1917
|
+
)
|
|
1918
|
+
url = build_debug_url(endpoint, base_path, rendered_path)
|
|
1919
|
+
|
|
1920
|
+
print("AIDP CLI debug request:", file=sys.stderr)
|
|
1921
|
+
print(f" command_group: {command_group}", file=sys.stderr)
|
|
1922
|
+
print(f" command_name: {command_name}", file=sys.stderr)
|
|
1923
|
+
print(f" method: {method}", file=sys.stderr)
|
|
1924
|
+
print(f" endpoint: {endpoint}", file=sys.stderr)
|
|
1925
|
+
print(f" base_path: {base_path}", file=sys.stderr)
|
|
1926
|
+
print(f" url: {url}", file=sys.stderr)
|
|
1927
|
+
print(f" resource_path: {resource_path}", file=sys.stderr)
|
|
1928
|
+
print(f" path_params: {json.dumps(path_params, sort_keys=True, default=str)}", file=sys.stderr)
|
|
1929
|
+
print(f" query_params: {json.dumps(query_params, sort_keys=True, default=str)}", file=sys.stderr)
|
|
1930
|
+
print(f" headers: {json.dumps(safe_headers(header_params), sort_keys=True, default=str)}", file=sys.stderr)
|
|
1931
|
+
print(f" body: {body_debug_summary(body)}", file=sys.stderr)
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def get_call_value(name: str, position: int, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
|
1935
|
+
if name in kwargs:
|
|
1936
|
+
return kwargs[name]
|
|
1937
|
+
if len(args) > position:
|
|
1938
|
+
return args[position]
|
|
1939
|
+
return None
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
def render_resource_path(
|
|
1943
|
+
resource_path: str,
|
|
1944
|
+
path_params: dict[str, Any],
|
|
1945
|
+
*,
|
|
1946
|
+
strict_url_encoding: bool = False,
|
|
1947
|
+
) -> str:
|
|
1948
|
+
rendered = resource_path
|
|
1949
|
+
safe_chars = "" if strict_url_encoding else "/"
|
|
1950
|
+
for name, value in path_params.items():
|
|
1951
|
+
rendered = rendered.replace("{" + name + "}", quote(str(value), safe=safe_chars))
|
|
1952
|
+
return rendered
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
def should_render_strict_url_encoding(base_client: Any, request_value: Any) -> bool:
|
|
1956
|
+
should_enable = getattr(base_client, "should_enable_strict_url_encoding", None)
|
|
1957
|
+
if callable(should_enable):
|
|
1958
|
+
return bool(should_enable(request_value))
|
|
1959
|
+
if request_value is not None:
|
|
1960
|
+
return bool(request_value)
|
|
1961
|
+
return bool(getattr(base_client, "enable_strict_url_encoding", None))
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def build_debug_url(endpoint: str, base_path: str, rendered_path: str) -> str:
|
|
1965
|
+
endpoint = endpoint.rstrip("/")
|
|
1966
|
+
if base_path and endpoint.endswith(base_path):
|
|
1967
|
+
return f"{endpoint}{rendered_path}"
|
|
1968
|
+
return f"{endpoint}{base_path}{rendered_path}"
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def safe_headers(header_params: dict[str, Any]) -> dict[str, Any]:
|
|
1972
|
+
sensitive = {"authorization", "x-content-sha256", "security-token"}
|
|
1973
|
+
return {
|
|
1974
|
+
name: ("<redacted>" if name.lower() in sensitive else value)
|
|
1975
|
+
for name, value in header_params.items()
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def body_debug_summary(body: Any) -> str:
|
|
1980
|
+
if body is None:
|
|
1981
|
+
return "none"
|
|
1982
|
+
if isinstance(body, dict):
|
|
1983
|
+
return "json object"
|
|
1984
|
+
if isinstance(body, list):
|
|
1985
|
+
return "json array"
|
|
1986
|
+
return type(body).__name__
|
|
1987
|
+
|
|
1988
|
+
|
|
1989
|
+
def print_response(response: Any) -> None:
|
|
1990
|
+
data = getattr(response, "data", response)
|
|
1991
|
+
payload = {
|
|
1992
|
+
"data": to_dict(data),
|
|
1993
|
+
"headers": dict(getattr(response, "headers", {}) or {}),
|
|
1994
|
+
"status": getattr(response, "status", None),
|
|
1995
|
+
}
|
|
1996
|
+
print("Response:")
|
|
1997
|
+
print(json.dumps(payload, indent=2, default=str))
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
def print_error_response(payload: dict[str, Any]) -> None:
|
|
2001
|
+
print("Response:", file=sys.stderr)
|
|
2002
|
+
print(json.dumps(payload, indent=2, default=str), file=sys.stderr)
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
if __name__ == "__main__":
|
|
2006
|
+
raise SystemExit(main())
|