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.
Files changed (56) hide show
  1. cu_cli/__init__.py +17 -0
  2. cu_cli/__main__.py +11 -0
  3. cu_cli/apiversion.py +124 -0
  4. cu_cli/cli.py +138 -0
  5. cu_cli/client.py +138 -0
  6. cu_cli/commands/__init__.py +4 -0
  7. cu_cli/commands/_command_spec.py +94 -0
  8. cu_cli/commands/_help.py +33 -0
  9. cu_cli/commands/_infra_models.py +184 -0
  10. cu_cli/commands/_infra_wizard.py +630 -0
  11. cu_cli/commands/_model_setup.py +46 -0
  12. cu_cli/commands/_options.py +112 -0
  13. cu_cli/commands/analyze.py +631 -0
  14. cu_cli/commands/analyzer.py +1462 -0
  15. cu_cli/commands/defaults.py +172 -0
  16. cu_cli/commands/doctor.py +166 -0
  17. cu_cli/commands/env_var.py +67 -0
  18. cu_cli/commands/infra.py +302 -0
  19. cu_cli/commands/profile_cmd.py +525 -0
  20. cu_cli/commands/upgrade.py +120 -0
  21. cu_cli/core/__init__.py +17 -0
  22. cu_cli/core/analyze.py +44 -0
  23. cu_cli/core/analyzers.py +30 -0
  24. cu_cli/core/azure_resources.py +486 -0
  25. cu_cli/core/defaults.py +18 -0
  26. cu_cli/core/doctor.py +42 -0
  27. cu_cli/core/foundry.py +68 -0
  28. cu_cli/core/infra_models.py +367 -0
  29. cu_cli/core/inputs.py +209 -0
  30. cu_cli/core/schema.py +24 -0
  31. cu_cli/errors.py +174 -0
  32. cu_cli/exit_codes.py +20 -0
  33. cu_cli/modality.py +24 -0
  34. cu_cli/output.py +179 -0
  35. cu_cli/profile.py +30 -0
  36. cu_cli/py.typed +0 -0
  37. cu_cli/resources/__init__.py +4 -0
  38. cu_cli/resources/azd_template/README.md +187 -0
  39. cu_cli/resources/azd_template/azure.yaml +27 -0
  40. cu_cli/resources/azd_template/hooks/postprovision.ps1 +320 -0
  41. cu_cli/resources/azd_template/hooks/postprovision.sh +299 -0
  42. cu_cli/resources/azd_template/infra/main.bicep +115 -0
  43. cu_cli/resources/azd_template/infra/main.parameters.json +30 -0
  44. cu_cli/resources/azd_template/infra/models.json +1 -0
  45. cu_cli/resources/azd_template/infra/modules/foundry.bicep +122 -0
  46. cu_cli/schema_validate.py +28 -0
  47. cu_cli/spec_validate.py +18 -0
  48. cu_cli/telemetry.py +42 -0
  49. cu_cli/update_check.py +154 -0
  50. cu_cli/update_provider.py +92 -0
  51. cu_cli/windows_self_upgrade.py +245 -0
  52. cu_cli-0.1.0b1.dist-info/METADATA +345 -0
  53. cu_cli-0.1.0b1.dist-info/RECORD +56 -0
  54. cu_cli-0.1.0b1.dist-info/WHEEL +5 -0
  55. cu_cli-0.1.0b1.dist-info/entry_points.txt +3 -0
  56. 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
@@ -0,0 +1,11 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Enable `python -m cu_cli`."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from .cli import main
9
+
10
+ if __name__ == "__main__":
11
+ main()
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,4 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """CLI command modules."""
@@ -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
@@ -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)