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
cu_cli/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""CU CLI — Azure Content Understanding Command Line Interface.
|
|
5
|
+
|
|
6
|
+
A deterministic, no-LLM CLI over the Content Understanding SDK for authoring,
|
|
7
|
+
validating, and running custom analyzers. See
|
|
8
|
+
https://github.com/Azure/content-understanding-toolkit/tree/main/cu-cli.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import version
|
|
14
|
+
|
|
15
|
+
__version__ = version("cu-cli")
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
cu_cli/__main__.py
ADDED
cu_cli/apiversion.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Central API-version resolution and validation.
|
|
5
|
+
|
|
6
|
+
The CLI explicitly tests ``2025-11-01`` and ``2026-06-01-preview``. Any
|
|
7
|
+
``YYYY-MM-DD-preview`` version is also accepted for forward compatibility,
|
|
8
|
+
while feature detection remains limited to explicitly known versions.
|
|
9
|
+
|
|
10
|
+
Resolution precedence (highest -> lowest):
|
|
11
|
+
1. ``--api-version`` flag on the command (per-invocation override)
|
|
12
|
+
2. schema-pinned ``apiVersion`` (schema-scoped commands only)
|
|
13
|
+
3. ``CU_API_VERSION`` environment variable
|
|
14
|
+
4. selected or active profile
|
|
15
|
+
5. built-in default (``2025-11-01`` GA)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from cu_cli_core.service_options import DEFAULT_API_VERSION
|
|
26
|
+
|
|
27
|
+
from .errors import CuCliError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ApiVersion:
|
|
32
|
+
value: str
|
|
33
|
+
label: str
|
|
34
|
+
features: frozenset[str] = frozenset()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
API_VERSIONS = (
|
|
38
|
+
ApiVersion("2025-11-01", "GA"),
|
|
39
|
+
ApiVersion(
|
|
40
|
+
"2026-06-01-preview",
|
|
41
|
+
"preview",
|
|
42
|
+
frozenset({"inline-analysis"}),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
SUPPORTED_API_VERSIONS = tuple(item.value for item in API_VERSIONS)
|
|
47
|
+
_PREVIEW_VERSION_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-preview$")
|
|
48
|
+
_KNOWN_VERSION_LABELS = tuple(
|
|
49
|
+
f"{item.value} ({item.label})" for item in API_VERSIONS
|
|
50
|
+
)
|
|
51
|
+
_KNOWN_VERSIONS_HELP = (
|
|
52
|
+
_KNOWN_VERSION_LABELS[0]
|
|
53
|
+
if len(_KNOWN_VERSION_LABELS) == 1
|
|
54
|
+
else f"{', '.join(_KNOWN_VERSION_LABELS[:-1])} and {_KNOWN_VERSION_LABELS[-1]}"
|
|
55
|
+
)
|
|
56
|
+
API_VERSION_HELP = (
|
|
57
|
+
f"Content Understanding API version. Known versions: {_KNOWN_VERSIONS_HELP}; "
|
|
58
|
+
"any YYYY-MM-DD-preview version is also accepted."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if API_VERSIONS[0].value != DEFAULT_API_VERSION:
|
|
62
|
+
raise RuntimeError("default API version must be the first supported API version")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _supported_list() -> str:
|
|
66
|
+
return ", ".join(f"{item.value} ({item.label})" for item in API_VERSIONS)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_preview_version(version: Optional[str]) -> bool:
|
|
70
|
+
"""Return whether *version* has the forward-compatible preview shape."""
|
|
71
|
+
return bool(version and _PREVIEW_VERSION_PATTERN.fullmatch(version))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_supported(version: Optional[str]) -> bool:
|
|
75
|
+
return version in SUPPORTED_API_VERSIONS or is_preview_version(version)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def ensure_supported(version: Optional[str]) -> str:
|
|
79
|
+
"""Return *version* if supported, else raise the design-doc error."""
|
|
80
|
+
if not is_supported(version):
|
|
81
|
+
raise CuCliError(
|
|
82
|
+
f"API version {version} is not supported by this CLI build. "
|
|
83
|
+
f"Supported: {_supported_list()} (or any YYYY-MM-DD-preview version).",
|
|
84
|
+
)
|
|
85
|
+
assert version is not None
|
|
86
|
+
return version
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def supports_api_feature(version: Optional[str], feature: str) -> bool:
|
|
90
|
+
"""Return whether an explicitly supported API version provides *feature*."""
|
|
91
|
+
canonical = ensure_supported(version)
|
|
92
|
+
return any(item.value == canonical and feature in item.features for item in API_VERSIONS)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_api_version(
|
|
96
|
+
*,
|
|
97
|
+
flag: Optional[str] = None,
|
|
98
|
+
schema_pinned: Optional[str] = None,
|
|
99
|
+
profile: Optional[str] = None,
|
|
100
|
+
env: Optional[str] = None,
|
|
101
|
+
default: str = DEFAULT_API_VERSION,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Resolve the effective api-version per the precedence chain.
|
|
104
|
+
|
|
105
|
+
``env`` defaults to ``CU_API_VERSION`` when not passed explicitly.
|
|
106
|
+
|
|
107
|
+
When both ``flag`` and ``schema_pinned`` are present and disagree, this is a
|
|
108
|
+
hard, fail-fast conflict: schema-scoped commands
|
|
109
|
+
must never silently diverge from the version their schema declares.
|
|
110
|
+
"""
|
|
111
|
+
if env is None:
|
|
112
|
+
env = os.getenv("CU_API_VERSION")
|
|
113
|
+
|
|
114
|
+
if flag is not None and schema_pinned is not None and flag != schema_pinned:
|
|
115
|
+
raise CuCliError(
|
|
116
|
+
f"Schema pins apiVersion '{schema_pinned}' but --api-version "
|
|
117
|
+
f"'{flag}' was passed. Remove the flag or align the schema.",
|
|
118
|
+
exit_code=2,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for candidate in (flag, schema_pinned, env, profile):
|
|
122
|
+
if candidate:
|
|
123
|
+
return ensure_supported(candidate)
|
|
124
|
+
return ensure_supported(default)
|
cu_cli/cli.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Top-level CLI: registers all command groups under ``cu`` / ``cu-cli``.
|
|
5
|
+
|
|
6
|
+
Uses ``rich-click`` for the help UX. Running ``cu`` with no subcommand prints
|
|
7
|
+
the full help.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import rich_click as click
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .apiversion import API_VERSION_HELP
|
|
18
|
+
from .commands.analyze import cmd_analyze
|
|
19
|
+
from .commands.analyzer import analyzer_group
|
|
20
|
+
from .commands.profile_cmd import profile_group
|
|
21
|
+
from .commands.defaults import defaults_group
|
|
22
|
+
from .commands.doctor import cmd_doctor
|
|
23
|
+
from .commands.env_var import env_var_group
|
|
24
|
+
from .commands.infra import infra_group
|
|
25
|
+
from .commands.upgrade import cmd_upgrade
|
|
26
|
+
from .commands._help import common_commands
|
|
27
|
+
from .commands._infra_models import cmd_infra_models
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _force_utf8_io() -> None:
|
|
31
|
+
"""Make stdout/stderr UTF-8 so redirects on Windows cmd don't crash."""
|
|
32
|
+
for stream in (sys.stdout, sys.stderr):
|
|
33
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
34
|
+
if reconfigure is not None:
|
|
35
|
+
try:
|
|
36
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_force_utf8_io()
|
|
42
|
+
|
|
43
|
+
# --- rich-click look & feel ------------------------------------------------
|
|
44
|
+
_rc = click.rich_click
|
|
45
|
+
_rc.MAX_WIDTH = 110
|
|
46
|
+
_rc.TEXT_MARKUP = "rich"
|
|
47
|
+
_rc.STYLE_OPTION = "bold cyan"
|
|
48
|
+
_rc.STYLE_COMMAND = "bold magenta"
|
|
49
|
+
_rc.STYLE_SWITCH = "bold cyan"
|
|
50
|
+
_rc.HEADER_TEXT = (
|
|
51
|
+
"[bold cyan]CU CLI[/] — [white]Azure Content Understanding Command Line Interface[/]"
|
|
52
|
+
)
|
|
53
|
+
_rc.FOOTER_TEXT = ""
|
|
54
|
+
|
|
55
|
+
_COMMAND_GROUPS: list = [
|
|
56
|
+
{"name": "Setup", "commands": ["infra", "profile", "doctor", "env-var"]},
|
|
57
|
+
{"name": "Content Understanding", "commands": ["analyze", "analyzer", "defaults"]},
|
|
58
|
+
{"name": "Maintenance", "commands": ["upgrade"]},
|
|
59
|
+
]
|
|
60
|
+
_rc.COMMAND_GROUPS = {"cu": _COMMAND_GROUPS, "cu-cli": _COMMAND_GROUPS}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
CLI_HELP = (
|
|
64
|
+
"Use Azure Content Understanding through a Microsoft Foundry resource to process files from the "
|
|
65
|
+
"terminal. An analyzer processes a document, image, audio file, or video and "
|
|
66
|
+
"returns an analyzer result with extracted content and structured fields. Use "
|
|
67
|
+
"a ready-to-use prebuilt analyzer or create a custom analyzer for your scenario.\n\n"
|
|
68
|
+
"[white] [/white]\n\n"
|
|
69
|
+
"`cu infra generate` generates an azd/Bicep project used to provision the required "
|
|
70
|
+
"Microsoft Foundry resource, optionally deploy supported large language models "
|
|
71
|
+
"(LLMs) and embeddings models, and configure Content Understanding defaults. "
|
|
72
|
+
"The command writes files only; `azd up` performs the Azure provisioning. "
|
|
73
|
+
"A local "
|
|
74
|
+
"CU CLI profile stores the resource endpoint, authentication method, API version, "
|
|
75
|
+
"and model mappings."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
class OrderedHelpGroup(click.RichGroup):
|
|
79
|
+
"""Preserve command registration order in help output."""
|
|
80
|
+
|
|
81
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
82
|
+
return list(self.commands)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@click.group(
|
|
86
|
+
cls=OrderedHelpGroup,
|
|
87
|
+
help=CLI_HELP,
|
|
88
|
+
epilog=common_commands(
|
|
89
|
+
(
|
|
90
|
+
"cu infra generate",
|
|
91
|
+
"Generate the azd/Bicep project used to provision and configure a Microsoft "
|
|
92
|
+
"Foundry resource. Run `azd up` from the generated directory to provision it.",
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"cu profile set endpoint https://<resource-name>.services.ai.azure.com/",
|
|
96
|
+
"Connect CU CLI to an existing Microsoft Foundry resource endpoint. Replace "
|
|
97
|
+
"<resource-name> with the name of your resource.",
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
"cu doctor",
|
|
101
|
+
"Verify the active profile, authentication, resource connectivity, and "
|
|
102
|
+
"Content Understanding defaults.",
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
"cu analyze sample.pdf -a prebuilt-layout",
|
|
106
|
+
"Analyze a PDF with the prebuilt-layout analyzer and print its extracted text, "
|
|
107
|
+
"document structure, and layout information as Markdown.",
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
+ "\n\n[white]For environment-variable help, run "
|
|
111
|
+
"[bold cyan]cu env-var -h[/bold cyan].[/white]\n\n"
|
|
112
|
+
f"[bold]{API_VERSION_HELP}[/bold]",
|
|
113
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
114
|
+
invoke_without_command=True,
|
|
115
|
+
)
|
|
116
|
+
@click.version_option(__version__, "-V", "--version", prog_name="cu")
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def main(ctx: click.Context) -> None:
|
|
119
|
+
"""Root group. A bare ``cu`` prints help."""
|
|
120
|
+
_force_utf8_io()
|
|
121
|
+
if ctx.invoked_subcommand is None:
|
|
122
|
+
click.echo(ctx.get_help())
|
|
123
|
+
ctx.exit(0)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
main.add_command(infra_group)
|
|
127
|
+
main.add_command(profile_group)
|
|
128
|
+
main.add_command(cmd_doctor)
|
|
129
|
+
main.add_command(env_var_group)
|
|
130
|
+
main.add_command(cmd_analyze)
|
|
131
|
+
main.add_command(analyzer_group)
|
|
132
|
+
main.add_command(defaults_group)
|
|
133
|
+
main.add_command(cmd_upgrade)
|
|
134
|
+
main.add_command(cmd_infra_models)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
cu_cli/client.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Build a ContentUnderstandingClient honoring config precedence + api-version.
|
|
5
|
+
|
|
6
|
+
The CLI is cloud-only: every client build requires an endpoint. The
|
|
7
|
+
``User-Agent`` telemetry header is stamped here so
|
|
8
|
+
every CU call carries it (opt-out honored — see ``telemetry.py``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from cu_cli_core.client import (
|
|
17
|
+
LRO_POLLING_INTERVAL_SECONDS,
|
|
18
|
+
build_content_understanding_client,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .apiversion import ensure_supported
|
|
22
|
+
from .core.foundry import normalize_foundry_endpoint
|
|
23
|
+
from .profile import Profile
|
|
24
|
+
from .errors import CuCliError
|
|
25
|
+
from .telemetry import user_agent
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ResolvedAuth:
|
|
29
|
+
endpoint: str
|
|
30
|
+
auth_mode: str # "entra" | "key"
|
|
31
|
+
api_version: str
|
|
32
|
+
api_key: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve(
|
|
36
|
+
profile: Profile,
|
|
37
|
+
*,
|
|
38
|
+
endpoint_override: Optional[str] = None,
|
|
39
|
+
api_key_override: Optional[str] = None,
|
|
40
|
+
api_version_override: Optional[str] = None,
|
|
41
|
+
force_entra: bool | str = False,
|
|
42
|
+
auth_mode_override: Optional[str] = None,
|
|
43
|
+
) -> ResolvedAuth:
|
|
44
|
+
endpoint = endpoint_override or profile.endpoint
|
|
45
|
+
if not endpoint:
|
|
46
|
+
raise CuCliError(
|
|
47
|
+
f"No CU endpoint configured for CU CLI profile '{profile.profile_name}'.",
|
|
48
|
+
hint="set one with `cu profile set endpoint <URL>`, select another saved "
|
|
49
|
+
"CU CLI profile with `cu profile set-active <name>`, or pass "
|
|
50
|
+
"`--endpoint`. Run `cu profile show` to inspect the effective profile.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
api_version = ensure_supported(api_version_override or profile.api_version)
|
|
54
|
+
|
|
55
|
+
requested_mode = auth_mode_override
|
|
56
|
+
if requested_mode is None and isinstance(force_entra, str):
|
|
57
|
+
requested_mode = force_entra
|
|
58
|
+
elif requested_mode is None and force_entra:
|
|
59
|
+
requested_mode = "login"
|
|
60
|
+
|
|
61
|
+
endpoint = normalize_foundry_endpoint(
|
|
62
|
+
endpoint,
|
|
63
|
+
auth_mode=requested_mode or profile.auth_mode,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if requested_mode == "login":
|
|
67
|
+
return ResolvedAuth(endpoint=endpoint, auth_mode="entra",
|
|
68
|
+
api_version=api_version, api_key=None)
|
|
69
|
+
if api_key_override or requested_mode == "key":
|
|
70
|
+
key = api_key_override or profile.api_key
|
|
71
|
+
if not key:
|
|
72
|
+
raise CuCliError(
|
|
73
|
+
"--auth-mode key requires an API key.",
|
|
74
|
+
hint="pass --api-key or configure api_key in the selected CU CLI profile.",
|
|
75
|
+
)
|
|
76
|
+
return ResolvedAuth(endpoint=endpoint, auth_mode="key",
|
|
77
|
+
api_version=api_version, api_key=key)
|
|
78
|
+
if profile.auth_mode == "key":
|
|
79
|
+
if not profile.api_key:
|
|
80
|
+
raise CuCliError(
|
|
81
|
+
"auth is 'key' but no api_key is configured.",
|
|
82
|
+
hint="run 'cu profile set api_key <KEY>' or switch to login auth with "
|
|
83
|
+
"'cu profile set auth_mode login' (then 'az login').",
|
|
84
|
+
)
|
|
85
|
+
return ResolvedAuth(endpoint=endpoint, auth_mode="key",
|
|
86
|
+
api_version=api_version, api_key=profile.api_key)
|
|
87
|
+
return ResolvedAuth(endpoint=endpoint, auth_mode="entra",
|
|
88
|
+
api_version=api_version, api_key=None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_client(
|
|
92
|
+
profile: Profile,
|
|
93
|
+
*,
|
|
94
|
+
endpoint_override: Optional[str] = None,
|
|
95
|
+
api_key_override: Optional[str] = None,
|
|
96
|
+
api_version_override: Optional[str] = None,
|
|
97
|
+
force_entra: bool | str = False,
|
|
98
|
+
auth_mode_override: Optional[str] = None,
|
|
99
|
+
) -> Any:
|
|
100
|
+
"""Construct a ``ContentUnderstandingClient`` for the resolved auth/version."""
|
|
101
|
+
from azure.core.credentials import AzureKeyCredential
|
|
102
|
+
from azure.identity import DefaultAzureCredential
|
|
103
|
+
|
|
104
|
+
if api_key_override:
|
|
105
|
+
# The value came from --api-key on argv, which is visible to `ps` and
|
|
106
|
+
# recorded in shell history. Nudge users toward safer alternatives.
|
|
107
|
+
from .output import console
|
|
108
|
+
console.print(
|
|
109
|
+
"[yellow]warning:[/yellow] --api-key on the command line is visible via "
|
|
110
|
+
"`ps`/shell history. Prefer the CU_API_KEY env var or "
|
|
111
|
+
"`cu profile set api_key`."
|
|
112
|
+
)
|
|
113
|
+
if force_entra is True or force_entra == "login" or auth_mode_override == "login":
|
|
114
|
+
console.print(
|
|
115
|
+
"[dim]note:[/dim] --auth-mode login overrides --api-key; "
|
|
116
|
+
"the provided key is ignored."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
auth = resolve(
|
|
120
|
+
profile,
|
|
121
|
+
endpoint_override=endpoint_override,
|
|
122
|
+
api_key_override=api_key_override,
|
|
123
|
+
api_version_override=api_version_override,
|
|
124
|
+
force_entra=force_entra,
|
|
125
|
+
auth_mode_override=auth_mode_override,
|
|
126
|
+
)
|
|
127
|
+
credential = (
|
|
128
|
+
AzureKeyCredential(auth.api_key or "")
|
|
129
|
+
if auth.auth_mode == "key"
|
|
130
|
+
else DefaultAzureCredential()
|
|
131
|
+
)
|
|
132
|
+
return build_content_understanding_client(
|
|
133
|
+
endpoint=auth.endpoint,
|
|
134
|
+
credential=credential,
|
|
135
|
+
api_version=auth.api_version,
|
|
136
|
+
user_agent=user_agent(),
|
|
137
|
+
polling_interval=LRO_POLLING_INTERVAL_SECONDS,
|
|
138
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Translate framework-neutral command arguments into Click decorators."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import rich_click as click
|
|
13
|
+
|
|
14
|
+
from cu_cli_core.command_spec import ArgumentSpec, ArgumentValueType, CommandSpec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SIMPLE_CLICK_TYPES: dict[ArgumentValueType, Any] = {
|
|
18
|
+
ArgumentValueType.STRING: str,
|
|
19
|
+
ArgumentValueType.BOOLEAN: bool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _click_type(argument: ArgumentSpec) -> Any:
|
|
24
|
+
if argument.value_type is ArgumentValueType.INTEGER:
|
|
25
|
+
return click.IntRange(argument.minimum, argument.maximum)
|
|
26
|
+
if argument.value_type is ArgumentValueType.PATH:
|
|
27
|
+
return click.Path(
|
|
28
|
+
exists=argument.path_exists,
|
|
29
|
+
file_okay=argument.file_okay,
|
|
30
|
+
dir_okay=argument.dir_okay,
|
|
31
|
+
path_type=Path,
|
|
32
|
+
)
|
|
33
|
+
return _SIMPLE_CLICK_TYPES[argument.value_type]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _click_argument(argument: ArgumentSpec) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
37
|
+
metavar = argument.name if argument.required else f"[{argument.name}]"
|
|
38
|
+
return click.argument(
|
|
39
|
+
argument.parser_name,
|
|
40
|
+
required=argument.required,
|
|
41
|
+
type=_click_type(argument),
|
|
42
|
+
metavar=argument.metavar or metavar,
|
|
43
|
+
nargs=-1 if argument.repeatable else 1,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _click_option(
|
|
48
|
+
argument: ArgumentSpec,
|
|
49
|
+
*,
|
|
50
|
+
has_alternate_binding: bool,
|
|
51
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
52
|
+
declarations = (*argument.aliases, argument.name, argument.parser_name)
|
|
53
|
+
kwargs: dict[str, Any] = {
|
|
54
|
+
"required": argument.required and not has_alternate_binding,
|
|
55
|
+
"help": argument.help,
|
|
56
|
+
}
|
|
57
|
+
if argument.default is not None:
|
|
58
|
+
kwargs["default"] = argument.default
|
|
59
|
+
if argument.value_type is ArgumentValueType.BOOLEAN:
|
|
60
|
+
kwargs["is_flag"] = True
|
|
61
|
+
else:
|
|
62
|
+
kwargs["type"] = (
|
|
63
|
+
click.Choice(argument.choices)
|
|
64
|
+
if argument.choices
|
|
65
|
+
else _click_type(argument)
|
|
66
|
+
)
|
|
67
|
+
kwargs["multiple"] = argument.repeatable
|
|
68
|
+
if argument.metavar is not None:
|
|
69
|
+
kwargs["metavar"] = argument.metavar
|
|
70
|
+
return click.option(*declarations, **kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def with_command_arguments(spec: CommandSpec):
|
|
74
|
+
"""Attach the command-specific arguments declared by ``spec``."""
|
|
75
|
+
|
|
76
|
+
def decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
77
|
+
fields_with_alternates = {
|
|
78
|
+
argument.field
|
|
79
|
+
for argument in spec.arguments
|
|
80
|
+
if sum(item.field == argument.field for item in spec.arguments) > 1
|
|
81
|
+
}
|
|
82
|
+
for argument in reversed(spec.arguments):
|
|
83
|
+
option = (
|
|
84
|
+
_click_argument(argument)
|
|
85
|
+
if argument.positional
|
|
86
|
+
else _click_option(
|
|
87
|
+
argument,
|
|
88
|
+
has_alternate_binding=argument.field in fields_with_alternates,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
fn = option(fn)
|
|
92
|
+
return fn
|
|
93
|
+
|
|
94
|
+
return decorate
|
cu_cli/commands/_help.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Shared rendering for command examples in terminal help."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def common_commands(*examples: tuple[str, str]) -> str:
|
|
12
|
+
"""Render command-first examples with consistent syntax highlighting."""
|
|
13
|
+
blocks = ["[bold cyan]Common commands:[/bold cyan]"]
|
|
14
|
+
for command, description in examples:
|
|
15
|
+
tokens = command.split()
|
|
16
|
+
styled: list[str] = []
|
|
17
|
+
command_path = True
|
|
18
|
+
for token in tokens:
|
|
19
|
+
safe = escape(token)
|
|
20
|
+
if token.startswith("-"):
|
|
21
|
+
style = "bold cyan"
|
|
22
|
+
command_path = False
|
|
23
|
+
elif token.isupper() or any(part.isupper() for part in token.split(".")):
|
|
24
|
+
style = "bold yellow"
|
|
25
|
+
command_path = False
|
|
26
|
+
elif command_path:
|
|
27
|
+
style = "bold green"
|
|
28
|
+
else:
|
|
29
|
+
style = "bold magenta"
|
|
30
|
+
styled.append(f"[{style}]{safe}[/{style}]")
|
|
31
|
+
blocks.append(" ".join(styled))
|
|
32
|
+
blocks.append(f"[white]\u00a0\u00a0{escape(description)}[/white]")
|
|
33
|
+
return "\n\n".join(blocks)
|