cu-cli 0.1.0b1__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.
- cu_cli/__init__.py +17 -0
- cu_cli/__main__.py +11 -0
- cu_cli/apiversion.py +124 -0
- cu_cli/cli.py +138 -0
- cu_cli/client.py +138 -0
- cu_cli/commands/__init__.py +4 -0
- cu_cli/commands/_command_spec.py +94 -0
- cu_cli/commands/_help.py +33 -0
- cu_cli/commands/_infra_models.py +184 -0
- cu_cli/commands/_infra_wizard.py +630 -0
- cu_cli/commands/_model_setup.py +46 -0
- cu_cli/commands/_options.py +112 -0
- cu_cli/commands/analyze.py +631 -0
- cu_cli/commands/analyzer.py +1462 -0
- cu_cli/commands/defaults.py +172 -0
- cu_cli/commands/doctor.py +166 -0
- cu_cli/commands/env_var.py +67 -0
- cu_cli/commands/infra.py +302 -0
- cu_cli/commands/profile_cmd.py +525 -0
- cu_cli/commands/upgrade.py +120 -0
- cu_cli/core/__init__.py +17 -0
- cu_cli/core/analyze.py +44 -0
- cu_cli/core/analyzers.py +30 -0
- cu_cli/core/azure_resources.py +486 -0
- cu_cli/core/defaults.py +18 -0
- cu_cli/core/doctor.py +42 -0
- cu_cli/core/foundry.py +68 -0
- cu_cli/core/infra_models.py +367 -0
- cu_cli/core/inputs.py +209 -0
- cu_cli/core/schema.py +24 -0
- cu_cli/errors.py +174 -0
- cu_cli/exit_codes.py +20 -0
- cu_cli/modality.py +24 -0
- cu_cli/output.py +179 -0
- cu_cli/profile.py +30 -0
- cu_cli/py.typed +0 -0
- cu_cli/resources/__init__.py +4 -0
- cu_cli/resources/azd_template/README.md +187 -0
- cu_cli/resources/azd_template/azure.yaml +27 -0
- cu_cli/resources/azd_template/hooks/postprovision.ps1 +320 -0
- cu_cli/resources/azd_template/hooks/postprovision.sh +299 -0
- cu_cli/resources/azd_template/infra/main.bicep +115 -0
- cu_cli/resources/azd_template/infra/main.parameters.json +30 -0
- cu_cli/resources/azd_template/infra/models.json +1 -0
- cu_cli/resources/azd_template/infra/modules/foundry.bicep +122 -0
- cu_cli/schema_validate.py +28 -0
- cu_cli/spec_validate.py +18 -0
- cu_cli/telemetry.py +42 -0
- cu_cli/update_check.py +154 -0
- cu_cli/update_provider.py +92 -0
- cu_cli/windows_self_upgrade.py +245 -0
- cu_cli-0.1.0b1.dist-info/METADATA +345 -0
- cu_cli-0.1.0b1.dist-info/RECORD +56 -0
- cu_cli-0.1.0b1.dist-info/WHEEL +5 -0
- cu_cli-0.1.0b1.dist-info/entry_points.txt +3 -0
- cu_cli-0.1.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""``cu profile`` — manage resource-specific settings in Azure CLI configuration."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import rich_click as click
|
|
14
|
+
from azure.core.exceptions import HttpResponseError
|
|
15
|
+
from click.exceptions import Exit
|
|
16
|
+
from cu_cli_core.command_spec import (
|
|
17
|
+
PROFILE_COPY,
|
|
18
|
+
PROFILE_CREATE,
|
|
19
|
+
PROFILE_DELETE,
|
|
20
|
+
PROFILE_GET,
|
|
21
|
+
PROFILE_LIST,
|
|
22
|
+
PROFILE_RENAME,
|
|
23
|
+
PROFILE_SET,
|
|
24
|
+
PROFILE_SET_ACTIVE,
|
|
25
|
+
PROFILE_SHOW,
|
|
26
|
+
PROFILE_SYNC_DEFAULTS,
|
|
27
|
+
PROFILE_UNSET,
|
|
28
|
+
CommandBindingError,
|
|
29
|
+
build_request,
|
|
30
|
+
resolve_identifier,
|
|
31
|
+
)
|
|
32
|
+
from cu_cli_core.defaults import (
|
|
33
|
+
extract_model_deployments,
|
|
34
|
+
with_prebuilt_default_mappings,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from ..apiversion import ensure_supported
|
|
38
|
+
from ..client import build_client
|
|
39
|
+
from ..core.foundry import (
|
|
40
|
+
endpoint_host,
|
|
41
|
+
host_label,
|
|
42
|
+
normalize_foundry_endpoint,
|
|
43
|
+
)
|
|
44
|
+
from ..core.defaults import is_defaults_not_set
|
|
45
|
+
from ..errors import CuCliError, friendly_errors
|
|
46
|
+
from ..exit_codes import GENERIC_ERROR, VALIDATION_FAILURE
|
|
47
|
+
from ..output import console, kv_table, result_console
|
|
48
|
+
from ..profile import Profile, ProfileStore
|
|
49
|
+
from ._command_spec import with_command_arguments
|
|
50
|
+
from ._help import common_commands
|
|
51
|
+
from ._model_setup import print_model_free_analyzers, print_model_setup_steps
|
|
52
|
+
from ._options import CALLING_TIME_OPTION, calling_time
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _request(spec, values: dict[str, Any]):
|
|
56
|
+
try:
|
|
57
|
+
return build_request(spec, values)
|
|
58
|
+
except CommandBindingError as exc:
|
|
59
|
+
raise CuCliError(str(exc), exit_code=VALIDATION_FAILURE) from exc
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_foundry_account(endpoint: str) -> tuple[str, str] | None:
|
|
63
|
+
az = shutil.which("az")
|
|
64
|
+
if not az:
|
|
65
|
+
return None
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[az, "cognitiveservices", "account", "list", "-o", "json"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
accounts = json.loads(result.stdout or "[]")
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
target = endpoint.rstrip("/").lower()
|
|
79
|
+
target_host = endpoint_host(endpoint)
|
|
80
|
+
target_label = host_label(target_host)
|
|
81
|
+
for account in accounts:
|
|
82
|
+
if not isinstance(account, dict):
|
|
83
|
+
continue
|
|
84
|
+
properties_value = account.get("properties")
|
|
85
|
+
properties: dict[str, Any] = (
|
|
86
|
+
properties_value if isinstance(properties_value, dict) else {}
|
|
87
|
+
)
|
|
88
|
+
account_endpoint = str(
|
|
89
|
+
properties.get("endpoint") or account.get("endpoint") or ""
|
|
90
|
+
)
|
|
91
|
+
name = str(account.get("name") or "").strip()
|
|
92
|
+
resource_group = str(account.get("resourceGroup") or "").strip()
|
|
93
|
+
if not name or not resource_group:
|
|
94
|
+
continue
|
|
95
|
+
if account_endpoint.rstrip("/").lower() == target:
|
|
96
|
+
return name, resource_group
|
|
97
|
+
if target_host.endswith(".services.ai.azure.com"):
|
|
98
|
+
candidates = {name.lower()}
|
|
99
|
+
custom_subdomain = str(
|
|
100
|
+
properties.get("customSubDomainName") or ""
|
|
101
|
+
).strip().lower()
|
|
102
|
+
if custom_subdomain:
|
|
103
|
+
candidates.add(custom_subdomain)
|
|
104
|
+
properties_host = endpoint_host(
|
|
105
|
+
str(properties.get("endpoint") or "")
|
|
106
|
+
)
|
|
107
|
+
if properties_host:
|
|
108
|
+
candidates.add(host_label(properties_host))
|
|
109
|
+
if target_label and target_label in candidates:
|
|
110
|
+
return name, resource_group
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _live_foundry_deployments(profile: Profile) -> dict[str, str] | None:
|
|
115
|
+
if not profile.endpoint:
|
|
116
|
+
console.print(
|
|
117
|
+
"[yellow]warn:[/yellow] live deployments unavailable "
|
|
118
|
+
"(no endpoint is saved in this profile)."
|
|
119
|
+
)
|
|
120
|
+
return None
|
|
121
|
+
az = shutil.which("az")
|
|
122
|
+
if not az:
|
|
123
|
+
console.print(
|
|
124
|
+
"[yellow]warn:[/yellow] live deployments unavailable "
|
|
125
|
+
"(Azure CLI `az` was not found)."
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
resolved = _resolve_foundry_account(profile.endpoint)
|
|
129
|
+
if not resolved:
|
|
130
|
+
console.print(
|
|
131
|
+
"[yellow]warn:[/yellow] live deployments unavailable "
|
|
132
|
+
"(the Microsoft Foundry resource could not be resolved from the endpoint)."
|
|
133
|
+
)
|
|
134
|
+
return None
|
|
135
|
+
account_name, resource_group = resolved
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
[
|
|
138
|
+
az,
|
|
139
|
+
"cognitiveservices",
|
|
140
|
+
"account",
|
|
141
|
+
"deployment",
|
|
142
|
+
"list",
|
|
143
|
+
"--resource-group",
|
|
144
|
+
resource_group,
|
|
145
|
+
"--name",
|
|
146
|
+
account_name,
|
|
147
|
+
"--output",
|
|
148
|
+
"json",
|
|
149
|
+
],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
)
|
|
153
|
+
if result.returncode != 0:
|
|
154
|
+
console.print(
|
|
155
|
+
"[yellow]warn:[/yellow] live deployments unavailable "
|
|
156
|
+
f"(Azure CLI returned: {(result.stderr or 'unknown error').strip()})."
|
|
157
|
+
)
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
payload = json.loads(result.stdout or "[]")
|
|
161
|
+
except json.JSONDecodeError as exc:
|
|
162
|
+
raise CuCliError(
|
|
163
|
+
"Azure CLI returned invalid JSON while listing Foundry deployments."
|
|
164
|
+
) from exc
|
|
165
|
+
rows: dict[str, str] = {}
|
|
166
|
+
for item in payload:
|
|
167
|
+
if not isinstance(item, dict):
|
|
168
|
+
continue
|
|
169
|
+
deployment_name = str(item.get("name") or "").strip()
|
|
170
|
+
if not deployment_name:
|
|
171
|
+
continue
|
|
172
|
+
properties_value = item.get("properties")
|
|
173
|
+
properties: dict[str, Any] = (
|
|
174
|
+
properties_value if isinstance(properties_value, dict) else {}
|
|
175
|
+
)
|
|
176
|
+
model_value = properties.get("model")
|
|
177
|
+
model: dict[str, Any] = model_value if isinstance(model_value, dict) else {}
|
|
178
|
+
sku_value = item.get("sku")
|
|
179
|
+
sku: dict[str, Any] = sku_value if isinstance(sku_value, dict) else {}
|
|
180
|
+
rows[f"deployment={deployment_name}"] = (
|
|
181
|
+
f"model={model.get('name') or '?'}, version={model.get('version') or '?'}, "
|
|
182
|
+
f"sku={sku.get('name') or '?'}, capacity={sku.get('capacity') or '?'}"
|
|
183
|
+
)
|
|
184
|
+
return dict(sorted(rows.items()))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@click.group(
|
|
188
|
+
"profile",
|
|
189
|
+
help="Manage local CU CLI settings for Microsoft Foundry resources.",
|
|
190
|
+
epilog=common_commands(
|
|
191
|
+
("cu profile show", "Show the active CU CLI profile."),
|
|
192
|
+
("cu profile set endpoint URL", "Configure the default CU CLI profile."),
|
|
193
|
+
("cu profile create dev", "Create a CU CLI profile for another resource."),
|
|
194
|
+
("cu profile set-active dev", "Select a saved CU CLI profile."),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
def profile_group() -> None:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@profile_group.command(
|
|
202
|
+
"_has-values",
|
|
203
|
+
hidden=True,
|
|
204
|
+
epilog=common_commands(("cu profile show --name default", "Show the default profile.")),
|
|
205
|
+
)
|
|
206
|
+
@click.option("--name", "profile_name", default="default")
|
|
207
|
+
def cmd_has_values(profile_name: str) -> None:
|
|
208
|
+
if not ProfileStore.load().get_profile(profile_name):
|
|
209
|
+
raise click.exceptions.Exit(3)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@profile_group.command(
|
|
213
|
+
"show",
|
|
214
|
+
help=PROFILE_SHOW.help,
|
|
215
|
+
epilog=common_commands(
|
|
216
|
+
("cu profile show", "Show the active CU CLI profile."),
|
|
217
|
+
("cu profile show --name dev", "Inspect a CU CLI profile without activating it."),
|
|
218
|
+
("cu profile show --deployments", "Also list live Foundry deployments."),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
@with_command_arguments(PROFILE_SHOW)
|
|
222
|
+
@CALLING_TIME_OPTION
|
|
223
|
+
@friendly_errors
|
|
224
|
+
def cmd_show(
|
|
225
|
+
profile_name: str | None,
|
|
226
|
+
deployments: bool,
|
|
227
|
+
show_calling_time: bool,
|
|
228
|
+
) -> None:
|
|
229
|
+
request = _request(
|
|
230
|
+
PROFILE_SHOW,
|
|
231
|
+
{"profile_name": profile_name, "deployments": deployments},
|
|
232
|
+
)
|
|
233
|
+
active_name = ProfileStore.load().get_active_name()
|
|
234
|
+
profile = resolve_identifier(PROFILE_SHOW.operation)(request)
|
|
235
|
+
title = f"CU CLI profile: {profile.profile_name}"
|
|
236
|
+
if profile_name is None:
|
|
237
|
+
title += " (active)"
|
|
238
|
+
else:
|
|
239
|
+
console.print(f"[dim]view only; active CU CLI profile remains:[/dim] {active_name}")
|
|
240
|
+
result_console.print(kv_table(profile.to_public_dict(), title=title))
|
|
241
|
+
if deployments:
|
|
242
|
+
with calling_time(show_calling_time) as timer:
|
|
243
|
+
live = _live_foundry_deployments(profile)
|
|
244
|
+
if live:
|
|
245
|
+
result_console.print(kv_table(live, title="foundry deployments (live)"))
|
|
246
|
+
timer.print()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@profile_group.command(
|
|
250
|
+
"list",
|
|
251
|
+
help=PROFILE_LIST.help,
|
|
252
|
+
epilog=common_commands(("cu profile list", "List profiles and mark the active one.")),
|
|
253
|
+
)
|
|
254
|
+
@friendly_errors
|
|
255
|
+
def cmd_list() -> None:
|
|
256
|
+
request = build_request(PROFILE_LIST, {})
|
|
257
|
+
active, names = resolve_identifier(PROFILE_LIST.operation)(request)
|
|
258
|
+
rows = {name: "(active)" if name == active else "" for name in names}
|
|
259
|
+
result_console.print(kv_table(rows, title="cu profile list"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@profile_group.command(
|
|
263
|
+
"get",
|
|
264
|
+
help=PROFILE_GET.help,
|
|
265
|
+
epilog=common_commands(
|
|
266
|
+
("cu profile get endpoint", "Print the active CU CLI profile's endpoint."),
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
@with_command_arguments(PROFILE_GET)
|
|
270
|
+
@friendly_errors
|
|
271
|
+
def cmd_get(
|
|
272
|
+
profile_key: str | None,
|
|
273
|
+
positional_profile_key: str | None,
|
|
274
|
+
profile_name: str | None,
|
|
275
|
+
) -> None:
|
|
276
|
+
request = _request(PROFILE_GET, locals())
|
|
277
|
+
value = resolve_identifier(PROFILE_GET.operation)(request)
|
|
278
|
+
if request.key == "api_key" and value:
|
|
279
|
+
value = "***redacted***"
|
|
280
|
+
click.echo("" if value is None else str(value))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@profile_group.command(
|
|
284
|
+
"set",
|
|
285
|
+
help=PROFILE_SET.help,
|
|
286
|
+
epilog=common_commands(
|
|
287
|
+
("cu profile set endpoint URL", "Save the default CU CLI profile endpoint."),
|
|
288
|
+
(
|
|
289
|
+
"cu profile set auth_mode login --name dev",
|
|
290
|
+
"Use Azure login authentication for a named CU CLI profile.",
|
|
291
|
+
),
|
|
292
|
+
(
|
|
293
|
+
"cu profile set model_deployments.gpt-5.2 DEPLOYMENT",
|
|
294
|
+
"Save one model deployment mapping.",
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
@with_command_arguments(PROFILE_SET)
|
|
299
|
+
@friendly_errors
|
|
300
|
+
def cmd_set(
|
|
301
|
+
profile_key: str | None,
|
|
302
|
+
positional_profile_key: str | None,
|
|
303
|
+
profile_value: str | None,
|
|
304
|
+
positional_profile_value: str | None,
|
|
305
|
+
profile_name: str | None,
|
|
306
|
+
) -> None:
|
|
307
|
+
request = _request(PROFILE_SET, locals())
|
|
308
|
+
value = request.value
|
|
309
|
+
if request.key == "endpoint":
|
|
310
|
+
value = normalize_foundry_endpoint(value)
|
|
311
|
+
elif request.key == "api_version":
|
|
312
|
+
value = ensure_supported(value)
|
|
313
|
+
request = type(request)(key=request.key, value=value, name=request.name)
|
|
314
|
+
path = resolve_identifier(PROFILE_SET.operation)(request)
|
|
315
|
+
target = request.name or ProfileStore.load().get_active_name()
|
|
316
|
+
console.print(
|
|
317
|
+
f"[green]ok[/green] saved {request.key} for CU CLI profile '{target}' -> {path}"
|
|
318
|
+
)
|
|
319
|
+
if request.key == "endpoint":
|
|
320
|
+
console.print(
|
|
321
|
+
"[dim]next:[/dim] authenticate if needed, then run "
|
|
322
|
+
"`cu profile sync-defaults` to import Content Understanding defaults."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@profile_group.command(
|
|
327
|
+
"unset",
|
|
328
|
+
help=PROFILE_UNSET.help,
|
|
329
|
+
epilog=common_commands(
|
|
330
|
+
("cu profile unset api_key", "Remove a saved key and return to login auth."),
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
@with_command_arguments(PROFILE_UNSET)
|
|
334
|
+
@friendly_errors
|
|
335
|
+
def cmd_unset(
|
|
336
|
+
profile_key: str | None,
|
|
337
|
+
positional_profile_key: str | None,
|
|
338
|
+
profile_name: str | None,
|
|
339
|
+
) -> None:
|
|
340
|
+
request = _request(PROFILE_UNSET, locals())
|
|
341
|
+
path = resolve_identifier(PROFILE_UNSET.operation)(request)
|
|
342
|
+
target = request.name or ProfileStore.load().get_active_name()
|
|
343
|
+
console.print(
|
|
344
|
+
f"[green]ok[/green] unset {request.key} for CU CLI profile '{target}' -> {path}"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@profile_group.command(
|
|
349
|
+
"create",
|
|
350
|
+
help=PROFILE_CREATE.help,
|
|
351
|
+
epilog=common_commands(
|
|
352
|
+
("cu profile create dev", "Create an empty named profile."),
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
@with_command_arguments(PROFILE_CREATE)
|
|
356
|
+
@friendly_errors
|
|
357
|
+
def cmd_create(
|
|
358
|
+
profile_name: str | None,
|
|
359
|
+
positional_profile_name: str | None,
|
|
360
|
+
) -> None:
|
|
361
|
+
request = _request(PROFILE_CREATE, locals())
|
|
362
|
+
path = resolve_identifier(PROFILE_CREATE.operation)(request)
|
|
363
|
+
console.print(f"[green]ok[/green] created CU CLI profile '{request.name}' -> {path}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@profile_group.command(
|
|
367
|
+
"delete",
|
|
368
|
+
help=PROFILE_DELETE.help,
|
|
369
|
+
epilog=common_commands(
|
|
370
|
+
("cu profile delete dev", "Delete an inactive named profile."),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
@with_command_arguments(PROFILE_DELETE)
|
|
374
|
+
@friendly_errors
|
|
375
|
+
def cmd_delete(
|
|
376
|
+
profile_name: str | None,
|
|
377
|
+
positional_profile_name: str | None,
|
|
378
|
+
) -> None:
|
|
379
|
+
request = _request(PROFILE_DELETE, locals())
|
|
380
|
+
path = resolve_identifier(PROFILE_DELETE.operation)(request)
|
|
381
|
+
console.print(f"[green]ok[/green] deleted CU CLI profile '{request.name}' -> {path}")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@profile_group.command(
|
|
385
|
+
"copy",
|
|
386
|
+
help=PROFILE_COPY.help,
|
|
387
|
+
epilog=common_commands(
|
|
388
|
+
("cu profile copy dev test", "Copy one profile to a new name."),
|
|
389
|
+
(
|
|
390
|
+
"cu profile copy --source dev --destination test",
|
|
391
|
+
"Copy using canonical named selectors.",
|
|
392
|
+
),
|
|
393
|
+
(
|
|
394
|
+
"cu profile copy --destination test",
|
|
395
|
+
"Copy the active CU CLI profile.",
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
@with_command_arguments(PROFILE_COPY)
|
|
400
|
+
@friendly_errors
|
|
401
|
+
def cmd_copy(
|
|
402
|
+
source_profile: str | None,
|
|
403
|
+
destination_profile: str | None,
|
|
404
|
+
positional_source_profile: str | None,
|
|
405
|
+
positional_destination_profile: str | None,
|
|
406
|
+
) -> None:
|
|
407
|
+
request = _request(PROFILE_COPY, locals())
|
|
408
|
+
path, source = resolve_identifier(PROFILE_COPY.operation)(request)
|
|
409
|
+
console.print(
|
|
410
|
+
f"[green]ok[/green] copied CU CLI profile '{source}' -> "
|
|
411
|
+
f"'{request.destination}' -> {path}"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@profile_group.command(
|
|
416
|
+
"rename",
|
|
417
|
+
help=PROFILE_RENAME.help,
|
|
418
|
+
epilog=common_commands(
|
|
419
|
+
("cu profile rename dev prod", "Rename a saved CU CLI profile."),
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
@with_command_arguments(PROFILE_RENAME)
|
|
423
|
+
@friendly_errors
|
|
424
|
+
def cmd_rename(
|
|
425
|
+
source_profile: str | None,
|
|
426
|
+
destination_profile: str | None,
|
|
427
|
+
positional_source_profile: str | None,
|
|
428
|
+
positional_destination_profile: str | None,
|
|
429
|
+
) -> None:
|
|
430
|
+
request = _request(PROFILE_RENAME, locals())
|
|
431
|
+
path = resolve_identifier(PROFILE_RENAME.operation)(request)
|
|
432
|
+
console.print(
|
|
433
|
+
f"[green]ok[/green] renamed CU CLI profile '{request.source}' -> "
|
|
434
|
+
f"'{request.destination}' -> {path}"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@profile_group.command(
|
|
439
|
+
"set-active",
|
|
440
|
+
help=PROFILE_SET_ACTIVE.help,
|
|
441
|
+
epilog=common_commands(
|
|
442
|
+
("cu profile set-active dev", "Select a saved CU CLI profile."),
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
@with_command_arguments(PROFILE_SET_ACTIVE)
|
|
446
|
+
@friendly_errors
|
|
447
|
+
def cmd_set_active(
|
|
448
|
+
profile_name: str | None,
|
|
449
|
+
positional_profile_name: str | None,
|
|
450
|
+
) -> None:
|
|
451
|
+
request = _request(PROFILE_SET_ACTIVE, locals())
|
|
452
|
+
path = resolve_identifier(PROFILE_SET_ACTIVE.operation)(request)
|
|
453
|
+
console.print(f"[green]ok[/green] active CU CLI profile -> {request.name} -> {path}")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@profile_group.command(
|
|
457
|
+
"sync-defaults",
|
|
458
|
+
help=PROFILE_SYNC_DEFAULTS.help,
|
|
459
|
+
epilog=common_commands(
|
|
460
|
+
(
|
|
461
|
+
"cu profile sync-defaults --name dev",
|
|
462
|
+
"Refresh model mappings using the profile's saved endpoint.",
|
|
463
|
+
),
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
@with_command_arguments(PROFILE_SYNC_DEFAULTS)
|
|
467
|
+
@click.option("--auth-mode", type=click.Choice(["login", "key"]), default=None)
|
|
468
|
+
@click.option("--api-key", default=None, help="Override the profile API key.")
|
|
469
|
+
@CALLING_TIME_OPTION
|
|
470
|
+
@friendly_errors
|
|
471
|
+
def cmd_sync_defaults(
|
|
472
|
+
profile_name: str | None,
|
|
473
|
+
auth_mode: str | None,
|
|
474
|
+
api_key: str | None,
|
|
475
|
+
show_calling_time: bool,
|
|
476
|
+
) -> None:
|
|
477
|
+
request = _request(PROFILE_SYNC_DEFAULTS, {"profile_name": profile_name})
|
|
478
|
+
saved = Profile.load_saved(profile_name=request.name)
|
|
479
|
+
if not saved.endpoint:
|
|
480
|
+
raise CuCliError(
|
|
481
|
+
f"no endpoint is saved in CU CLI profile '{saved.profile_name}'.",
|
|
482
|
+
hint="set it with `cu profile set endpoint URL` before synchronizing.",
|
|
483
|
+
)
|
|
484
|
+
effective = Profile.load(profile_name=request.name)
|
|
485
|
+
effective.endpoint = saved.endpoint
|
|
486
|
+
with calling_time(show_calling_time) as timer:
|
|
487
|
+
try:
|
|
488
|
+
defaults = build_client(
|
|
489
|
+
effective,
|
|
490
|
+
api_key_override=api_key,
|
|
491
|
+
auth_mode_override=auth_mode,
|
|
492
|
+
).get_defaults()
|
|
493
|
+
except HttpResponseError as exc:
|
|
494
|
+
if not is_defaults_not_set(exc):
|
|
495
|
+
raise
|
|
496
|
+
_print_sync_defaults_not_configured(saved)
|
|
497
|
+
models = with_prebuilt_default_mappings(
|
|
498
|
+
extract_model_deployments(defaults)
|
|
499
|
+
)
|
|
500
|
+
if not models:
|
|
501
|
+
_print_sync_defaults_not_configured(saved)
|
|
502
|
+
path, target = resolve_identifier(PROFILE_SYNC_DEFAULTS.operation)(
|
|
503
|
+
request,
|
|
504
|
+
models,
|
|
505
|
+
)
|
|
506
|
+
console.print(
|
|
507
|
+
f"[green]ok[/green] synchronized {len(models)} model mapping(s) "
|
|
508
|
+
f"for CU CLI profile '{target}' -> {path}"
|
|
509
|
+
)
|
|
510
|
+
timer.print()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _print_sync_defaults_not_configured(profile: Profile) -> None:
|
|
514
|
+
console.print(
|
|
515
|
+
"[yellow]Content Understanding defaults are not configured on this "
|
|
516
|
+
"resource.[/yellow]\n\n"
|
|
517
|
+
f"No changes were made to CU CLI profile '{profile.profile_name}'."
|
|
518
|
+
)
|
|
519
|
+
print_model_free_analyzers()
|
|
520
|
+
print_model_setup_steps(
|
|
521
|
+
profile.endpoint or "",
|
|
522
|
+
profile_name=profile.profile_name,
|
|
523
|
+
heading="To use analyzers that require generative AI:",
|
|
524
|
+
)
|
|
525
|
+
raise Exit(GENERIC_ERROR)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""``cu upgrade`` — pip-convention self-update helper.
|
|
5
|
+
|
|
6
|
+
``cu upgrade --check`` reports whether the installed update provider has a newer
|
|
7
|
+
release without changing anything. ``cu upgrade`` prints the exact ``pip``
|
|
8
|
+
command and, on a TTY, offers to run it. The CLI **never auto-updates** — the
|
|
9
|
+
user is always in control.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
18
|
+
|
|
19
|
+
import rich_click as click
|
|
20
|
+
|
|
21
|
+
from .. import __version__
|
|
22
|
+
from ..errors import friendly_errors
|
|
23
|
+
from ..output import console
|
|
24
|
+
from ..update_check import fetch_latest_version_detailed, is_newer, upgrade_hint
|
|
25
|
+
from ..update_provider import get_update_provider, pip_install_args
|
|
26
|
+
from ..windows_self_upgrade import is_windows, run_windows_upgrade
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command(
|
|
30
|
+
"upgrade",
|
|
31
|
+
help="Check for and install a newer cu-cli release (never automatic).",
|
|
32
|
+
epilog="[bold cyan]Common commands:[/bold cyan]\n\n"
|
|
33
|
+
"[bold green]cu upgrade[/bold green] [bold cyan]--check[/bold cyan]\n\n"
|
|
34
|
+
"[white]\u00a0\u00a0Check for a newer release without installing it.[/white]\n\n"
|
|
35
|
+
"[bold green]cu upgrade[/bold green]\n\n"
|
|
36
|
+
"[white]\u00a0\u00a0Check for a newer release and offer to install it.[/white]",
|
|
37
|
+
)
|
|
38
|
+
@click.option("--check", is_flag=True,
|
|
39
|
+
help="Only report whether a newer version exists; don't install.")
|
|
40
|
+
@click.option("--yes", is_flag=True, help="Run the upgrade without prompting.")
|
|
41
|
+
@friendly_errors
|
|
42
|
+
def cmd_upgrade(check: bool, yes: bool) -> None:
|
|
43
|
+
console.print(f"[bold]current:[/bold] cu-cli {__version__}")
|
|
44
|
+
provider = get_update_provider()
|
|
45
|
+
latest, reason = fetch_latest_version_detailed(use_cache=False)
|
|
46
|
+
|
|
47
|
+
if latest is None:
|
|
48
|
+
if reason == "not_published":
|
|
49
|
+
console.print(f"[yellow]cu-cli is not published to {provider.name} yet.[/yellow]")
|
|
50
|
+
console.print(
|
|
51
|
+
f"[dim]install or update from source:[/dim] {provider.source_install_hint}"
|
|
52
|
+
)
|
|
53
|
+
elif reason == "disabled":
|
|
54
|
+
console.print("[dim]update checks are disabled (CU_NO_UPDATE_CHECK).[/dim]")
|
|
55
|
+
else:
|
|
56
|
+
console.print(
|
|
57
|
+
f"[yellow]could not reach {provider.name} to check for updates.[/yellow]"
|
|
58
|
+
)
|
|
59
|
+
console.print(f"[dim]check your network, or install from source:[/dim] "
|
|
60
|
+
f"{provider.source_install_hint}")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if not is_newer(latest):
|
|
64
|
+
console.print(f"[green]up to date[/green] (latest: {latest}).")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
console.print(upgrade_hint(latest, provider.release_notes_url))
|
|
68
|
+
pip_args = pip_install_args(latest)
|
|
69
|
+
if check:
|
|
70
|
+
# `--check` is report-only; exit 0 so scripts can parse the message.
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if not yes:
|
|
74
|
+
if not sys.stdin.isatty():
|
|
75
|
+
console.print(f"[dim]run:[/dim] {' '.join(pip_args)}")
|
|
76
|
+
return
|
|
77
|
+
if not click.confirm(f"Upgrade cu-cli {__version__} -> {latest} now?", default=True):
|
|
78
|
+
console.print(f"[dim]skipped. Upgrade later with:[/dim] {' '.join(pip_args)}")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
console.print(f"[dim]source:[/dim] {provider.name}")
|
|
82
|
+
|
|
83
|
+
if is_windows():
|
|
84
|
+
# On Windows, `cu.exe` holds an exclusive lock on its own executable
|
|
85
|
+
# image while running. `pip install --upgrade` is non-atomic
|
|
86
|
+
# (uninstall then install), so running it in-process here can
|
|
87
|
+
# uninstall the current cu-cli and then fail to replace the locked
|
|
88
|
+
# file, leaving the environment without an importable cu_cli at all
|
|
89
|
+
# (regression). Instead, hand off to a detached helper process that
|
|
90
|
+
# waits for this process to exit before upgrading, and rolls back to
|
|
91
|
+
# the current version automatically if the upgrade fails.
|
|
92
|
+
try:
|
|
93
|
+
core_version = _pkg_version("cu-cli-core")
|
|
94
|
+
except PackageNotFoundError:
|
|
95
|
+
core_version = None
|
|
96
|
+
exit_code, log_path = run_windows_upgrade(
|
|
97
|
+
current_version=__version__,
|
|
98
|
+
core_version=core_version,
|
|
99
|
+
pip_args=pip_args,
|
|
100
|
+
pip_env=provider.pip_environment(),
|
|
101
|
+
)
|
|
102
|
+
console.print(
|
|
103
|
+
"[dim]upgrade will continue after cu exits (Windows cannot replace "
|
|
104
|
+
"its own running executable in-process).[/dim]"
|
|
105
|
+
)
|
|
106
|
+
console.print(f"[dim]progress log:[/dim] {log_path}")
|
|
107
|
+
console.print(
|
|
108
|
+
f"[green]ok[/green] upgrade to {latest} started. "
|
|
109
|
+
f"Run [bold]cu --version[/bold] after a moment to confirm."
|
|
110
|
+
)
|
|
111
|
+
sys.exit(exit_code)
|
|
112
|
+
|
|
113
|
+
console.print(f"[dim]running:[/dim] {' '.join(pip_args)}")
|
|
114
|
+
result = subprocess.run(pip_args, env={**os.environ, **provider.pip_environment()})
|
|
115
|
+
if result.returncode == 0:
|
|
116
|
+
console.print(f"[green]ok[/green] upgraded to {latest}. "
|
|
117
|
+
f"Release notes: {provider.release_notes_url}")
|
|
118
|
+
else:
|
|
119
|
+
console.print("[yellow]pip exited non-zero; upgrade may not have completed.[/yellow]")
|
|
120
|
+
sys.exit(result.returncode)
|
cu_cli/core/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Click-free, reusable business logic for the CU CLI.
|
|
5
|
+
|
|
6
|
+
Modules under ``cu_cli.core`` contain the real work behind each command: input
|
|
7
|
+
discovery, the concurrent analyze engine, analyzer CRUD, schema authoring,
|
|
8
|
+
service defaults, doctor checks, and infrastructure-generation helpers.
|
|
9
|
+
|
|
10
|
+
The boundary rule: functions here accept an already-built SDK ``client`` and/or
|
|
11
|
+
plain parameters and return data or typed result objects. They never call
|
|
12
|
+
``console.print``, ``sys.exit``, ``click.confirm``, or ``click.prompt`` — all
|
|
13
|
+
presentation, prompting, and exit-code handling lives in ``cu_cli.commands``.
|
|
14
|
+
This keeps the logic independently testable and reusable from other tools.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|