cu-cli-core 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_core/__init__.py +12 -0
- cu_cli_core/analysis.py +374 -0
- cu_cli_core/client.py +39 -0
- cu_cli_core/command_spec.py +1099 -0
- cu_cli_core/contracts.py +404 -0
- cu_cli_core/defaults.py +150 -0
- cu_cli_core/environment.py +102 -0
- cu_cli_core/errors.py +77 -0
- cu_cli_core/input_planning.py +322 -0
- cu_cli_core/operations/__init__.py +4 -0
- cu_cli_core/operations/analysis.py +294 -0
- cu_cli_core/operations/analyzer_copy.py +327 -0
- cu_cli_core/operations/analyzers.py +85 -0
- cu_cli_core/operations/profiles.py +146 -0
- cu_cli_core/operations/schema.py +48 -0
- cu_cli_core/operations/validation.py +41 -0
- cu_cli_core/profiles.py +604 -0
- cu_cli_core/py.typed +0 -0
- cu_cli_core/resources/openapi/2025-11-01/ContentUnderstanding.json +3643 -0
- cu_cli_core/resources/openapi/2026-06-01-preview/ContentUnderstanding.json +4050 -0
- cu_cli_core/schema.py +389 -0
- cu_cli_core/schema_validation.py +312 -0
- cu_cli_core/serialization.py +75 -0
- cu_cli_core/service_options.py +71 -0
- cu_cli_core/spec_validation.py +141 -0
- cu_cli_core-0.1.0b1.dist-info/METADATA +26 -0
- cu_cli_core-0.1.0b1.dist-info/RECORD +29 -0
- cu_cli_core-0.1.0b1.dist-info/WHEEL +5 -0
- cu_cli_core-0.1.0b1.dist-info/top_level.txt +1 -0
cu_cli_core/contracts.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Typed request and planning contracts shared by CU command frontends."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Mapping
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResultView(str, Enum):
|
|
15
|
+
LLM_INPUT = "llm-input"
|
|
16
|
+
FULL = "full"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SelectionMode(str, Enum):
|
|
20
|
+
POSITIONAL = "positional"
|
|
21
|
+
NAMED_FILES = "named-files"
|
|
22
|
+
NAMED_SOURCES = "named-sources"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InputOrigin(str, Enum):
|
|
26
|
+
POSITIONAL_FILE = "positional-file"
|
|
27
|
+
POSITIONAL_SOURCE = "positional-source"
|
|
28
|
+
NAMED_FILE = "named-file"
|
|
29
|
+
NAMED_SOURCE = "named-source"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExistingResultPolicy(str, Enum):
|
|
33
|
+
ERROR = "error"
|
|
34
|
+
SKIP = "skip"
|
|
35
|
+
REANALYZE = "reanalyze"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuthMode(str, Enum):
|
|
39
|
+
LOGIN = "login"
|
|
40
|
+
KEY = "key"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OutcomeStatus(str, Enum):
|
|
44
|
+
SUCCEEDED = "succeeded"
|
|
45
|
+
FAILED = "failed"
|
|
46
|
+
SKIPPED = "skipped"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ProfileShowRequest:
|
|
51
|
+
name: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ProfileListRequest:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ProfileGetRequest:
|
|
61
|
+
key: str
|
|
62
|
+
name: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ProfileSetRequest:
|
|
67
|
+
key: str
|
|
68
|
+
value: str
|
|
69
|
+
name: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ProfileUnsetRequest:
|
|
74
|
+
key: str
|
|
75
|
+
name: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ProfileCreateRequest:
|
|
80
|
+
name: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ProfileDeleteRequest:
|
|
85
|
+
name: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class ProfileCopyRequest:
|
|
90
|
+
destination: str
|
|
91
|
+
source: str | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class ProfileRenameRequest:
|
|
96
|
+
source: str
|
|
97
|
+
destination: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class ProfileSetActiveRequest:
|
|
102
|
+
name: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class ProfileSyncModelsRequest:
|
|
107
|
+
name: str | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class AnalyzerShowRequest:
|
|
112
|
+
"""Canonical request for retrieving one analyzer."""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
normalized = self.name.strip()
|
|
118
|
+
if not normalized:
|
|
119
|
+
raise ValueError("analyzer name cannot be empty.")
|
|
120
|
+
object.__setattr__(self, "name", normalized)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class AnalyzerListRequest:
|
|
125
|
+
kind: str = "all"
|
|
126
|
+
sort_by: str = "analyzerId"
|
|
127
|
+
|
|
128
|
+
def __post_init__(self) -> None:
|
|
129
|
+
if self.kind not in {"all", "prebuilt", "custom"}:
|
|
130
|
+
raise ValueError(f"invalid analyzer kind: {self.kind}")
|
|
131
|
+
if self.sort_by not in {"analyzerId", "createdAt", "lastModifiedAt"}:
|
|
132
|
+
raise ValueError(f"invalid analyzer sort field: {self.sort_by}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class AnalyzerCreateRequest:
|
|
137
|
+
name: str
|
|
138
|
+
schema: Path
|
|
139
|
+
|
|
140
|
+
def __post_init__(self) -> None:
|
|
141
|
+
normalized = self.name.strip()
|
|
142
|
+
if not normalized:
|
|
143
|
+
raise ValueError("analyzer name cannot be empty.")
|
|
144
|
+
object.__setattr__(self, "name", normalized)
|
|
145
|
+
object.__setattr__(self, "schema", Path(self.schema))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class AnalyzerDeleteRequest:
|
|
150
|
+
name: str
|
|
151
|
+
yes: bool = False
|
|
152
|
+
|
|
153
|
+
def __post_init__(self) -> None:
|
|
154
|
+
normalized = self.name.strip()
|
|
155
|
+
if not normalized:
|
|
156
|
+
raise ValueError("analyzer name cannot be empty.")
|
|
157
|
+
object.__setattr__(self, "name", normalized)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class AnalyzeRequest:
|
|
162
|
+
positional_inputs: tuple[Path, ...] = ()
|
|
163
|
+
files: tuple[Path, ...] = ()
|
|
164
|
+
sources: tuple[Path, ...] = ()
|
|
165
|
+
pattern: str | None = None
|
|
166
|
+
recursive: bool = False
|
|
167
|
+
analyzer: str | None = None
|
|
168
|
+
inline: bool = False
|
|
169
|
+
usage: bool = False
|
|
170
|
+
llm_input: bool = False
|
|
171
|
+
output_file: Path | None = None
|
|
172
|
+
output_dir: Path | None = None
|
|
173
|
+
on_existing: ExistingResultPolicy | None = None
|
|
174
|
+
dry_run: bool = False
|
|
175
|
+
yes: bool = False
|
|
176
|
+
report_file: Path | None = None
|
|
177
|
+
concurrency: int = 4
|
|
178
|
+
|
|
179
|
+
def __post_init__(self) -> None:
|
|
180
|
+
object.__setattr__(
|
|
181
|
+
self,
|
|
182
|
+
"positional_inputs",
|
|
183
|
+
tuple(Path(path) for path in self.positional_inputs),
|
|
184
|
+
)
|
|
185
|
+
object.__setattr__(self, "files", tuple(Path(path) for path in self.files))
|
|
186
|
+
object.__setattr__(self, "sources", tuple(Path(path) for path in self.sources))
|
|
187
|
+
if self.output_file is not None:
|
|
188
|
+
object.__setattr__(self, "output_file", Path(self.output_file))
|
|
189
|
+
if self.output_dir is not None:
|
|
190
|
+
object.__setattr__(self, "output_dir", Path(self.output_dir))
|
|
191
|
+
if self.report_file is not None:
|
|
192
|
+
object.__setattr__(self, "report_file", Path(self.report_file))
|
|
193
|
+
if isinstance(self.on_existing, str):
|
|
194
|
+
object.__setattr__(
|
|
195
|
+
self,
|
|
196
|
+
"on_existing",
|
|
197
|
+
ExistingResultPolicy(self.on_existing),
|
|
198
|
+
)
|
|
199
|
+
if not 1 <= self.concurrency <= 32:
|
|
200
|
+
raise ValueError("concurrency must be between 1 and 32.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class AnalyzerTestRequest:
|
|
205
|
+
name: str
|
|
206
|
+
positional_inputs: tuple[Path, ...] = ()
|
|
207
|
+
files: tuple[Path, ...] = ()
|
|
208
|
+
sources: tuple[Path, ...] = ()
|
|
209
|
+
pattern: str | None = None
|
|
210
|
+
recursive: bool = False
|
|
211
|
+
dry_run: bool = False
|
|
212
|
+
output_file: Path | None = None
|
|
213
|
+
force: bool = False
|
|
214
|
+
yes: bool = False
|
|
215
|
+
concurrency: int = 4
|
|
216
|
+
|
|
217
|
+
def __post_init__(self) -> None:
|
|
218
|
+
normalized = self.name.strip()
|
|
219
|
+
if not normalized:
|
|
220
|
+
raise ValueError("analyzer name cannot be empty.")
|
|
221
|
+
object.__setattr__(self, "name", normalized)
|
|
222
|
+
object.__setattr__(
|
|
223
|
+
self,
|
|
224
|
+
"positional_inputs",
|
|
225
|
+
tuple(Path(path) for path in self.positional_inputs),
|
|
226
|
+
)
|
|
227
|
+
object.__setattr__(self, "files", tuple(Path(path) for path in self.files))
|
|
228
|
+
object.__setattr__(self, "sources", tuple(Path(path) for path in self.sources))
|
|
229
|
+
if self.output_file is not None:
|
|
230
|
+
object.__setattr__(self, "output_file", Path(self.output_file))
|
|
231
|
+
if not 1 <= self.concurrency <= 16:
|
|
232
|
+
raise ValueError("concurrency must be between 1 and 16.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True)
|
|
236
|
+
class AnalyzerCopyRequest:
|
|
237
|
+
source: str
|
|
238
|
+
destination: str
|
|
239
|
+
source_resource: str | None = None
|
|
240
|
+
source_subscription: str | None = None
|
|
241
|
+
source_resource_group: str | None = None
|
|
242
|
+
source_profile: str | None = None
|
|
243
|
+
destination_resource: str | None = None
|
|
244
|
+
destination_subscription: str | None = None
|
|
245
|
+
destination_resource_group: str | None = None
|
|
246
|
+
destination_profile: str | None = None
|
|
247
|
+
|
|
248
|
+
def __post_init__(self) -> None:
|
|
249
|
+
source = self.source.strip()
|
|
250
|
+
destination = self.destination.strip()
|
|
251
|
+
if not source or not destination:
|
|
252
|
+
raise ValueError("source and destination analyzer names are required.")
|
|
253
|
+
object.__setattr__(self, "source", source)
|
|
254
|
+
object.__setattr__(self, "destination", destination)
|
|
255
|
+
if self.source_resource and self.source_profile:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"--source-resource and --source-profile are mutually exclusive."
|
|
258
|
+
)
|
|
259
|
+
if self.destination_resource and self.destination_profile:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"--destination-resource and --destination-profile are mutually exclusive."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(frozen=True)
|
|
266
|
+
class AnalyzerValidateRequest:
|
|
267
|
+
schema: Path
|
|
268
|
+
strict: bool = False
|
|
269
|
+
spec: bool = False
|
|
270
|
+
api_version: str | None = None
|
|
271
|
+
|
|
272
|
+
def __post_init__(self) -> None:
|
|
273
|
+
object.__setattr__(self, "schema", Path(self.schema))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass(frozen=True)
|
|
277
|
+
class AnalyzerSchemaCreateRequest:
|
|
278
|
+
from_template: bool = False
|
|
279
|
+
from_sample: Path | None = None
|
|
280
|
+
name: str = "my_analyzer_v1"
|
|
281
|
+
base: str | None = None
|
|
282
|
+
modality: str = "document"
|
|
283
|
+
output_file: Path | None = None
|
|
284
|
+
template_type: str = "extraction"
|
|
285
|
+
force: bool = False
|
|
286
|
+
|
|
287
|
+
def __post_init__(self) -> None:
|
|
288
|
+
name = self.name.strip()
|
|
289
|
+
if not name:
|
|
290
|
+
raise ValueError("analyzer name cannot be empty.")
|
|
291
|
+
object.__setattr__(self, "name", name)
|
|
292
|
+
if self.from_sample is not None:
|
|
293
|
+
object.__setattr__(self, "from_sample", Path(self.from_sample))
|
|
294
|
+
if self.output_file is not None:
|
|
295
|
+
object.__setattr__(self, "output_file", Path(self.output_file))
|
|
296
|
+
if self.from_template and self.from_sample is not None:
|
|
297
|
+
raise ValueError("--from-template and --from-sample cannot be combined.")
|
|
298
|
+
if self.modality not in {"document", "image", "audio", "video"}:
|
|
299
|
+
raise ValueError(f"invalid modality: {self.modality}")
|
|
300
|
+
if self.template_type not in {"extraction", "classification"}:
|
|
301
|
+
raise ValueError(f"invalid schema type: {self.template_type}")
|
|
302
|
+
if self.from_sample is not None and (
|
|
303
|
+
self.template_type != "extraction"
|
|
304
|
+
or self.modality != "document"
|
|
305
|
+
or self.base is not None
|
|
306
|
+
):
|
|
307
|
+
raise ValueError(
|
|
308
|
+
"--from-sample cannot be combined with --type classification, "
|
|
309
|
+
"--modality, or --base."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True)
|
|
314
|
+
class DefaultsShowRequest:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class DefaultsSetRequest:
|
|
320
|
+
models: tuple[str, ...] = ()
|
|
321
|
+
from_profile: bool = False
|
|
322
|
+
replace: bool = False
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass(frozen=True)
|
|
326
|
+
class EnvironmentVariableListRequest:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass(frozen=True)
|
|
331
|
+
class PlannedInput:
|
|
332
|
+
path: Path
|
|
333
|
+
source_root: Path
|
|
334
|
+
relative_path: Path
|
|
335
|
+
origin: InputOrigin
|
|
336
|
+
size_bytes: int
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@dataclass(frozen=True)
|
|
340
|
+
class SkippedInput:
|
|
341
|
+
path: Path
|
|
342
|
+
reason: str
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True)
|
|
346
|
+
class InputPlan:
|
|
347
|
+
inputs: tuple[PlannedInput, ...]
|
|
348
|
+
mode: SelectionMode
|
|
349
|
+
recursive: bool
|
|
350
|
+
pattern: str | None
|
|
351
|
+
total_bytes: int
|
|
352
|
+
extension_counts: Mapping[str, int] = field(default_factory=dict)
|
|
353
|
+
skipped: tuple[SkippedInput, ...] = ()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(frozen=True)
|
|
357
|
+
class PlannedOutput:
|
|
358
|
+
source: PlannedInput
|
|
359
|
+
path: Path | None
|
|
360
|
+
exists: bool = False
|
|
361
|
+
skipped: bool = False
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@dataclass(frozen=True)
|
|
365
|
+
class ExecutionPlan:
|
|
366
|
+
input_plan: InputPlan
|
|
367
|
+
outputs: tuple[PlannedOutput, ...]
|
|
368
|
+
on_existing: ExistingResultPolicy
|
|
369
|
+
dry_run: bool
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass(frozen=True)
|
|
373
|
+
class ErrorDetail:
|
|
374
|
+
code: str | None = None
|
|
375
|
+
message: str | None = None
|
|
376
|
+
target: str | None = None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True)
|
|
380
|
+
class FileOutcome:
|
|
381
|
+
source: Path
|
|
382
|
+
status: OutcomeStatus
|
|
383
|
+
analyzer: str
|
|
384
|
+
output_path: Path | None = None
|
|
385
|
+
usage: Mapping[str, Any] | None = None
|
|
386
|
+
error: ErrorDetail | None = None
|
|
387
|
+
payload: Any = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@dataclass(frozen=True)
|
|
391
|
+
class BatchReport:
|
|
392
|
+
outcomes: tuple[FileOutcome, ...]
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def succeeded(self) -> int:
|
|
396
|
+
return sum(item.status is OutcomeStatus.SUCCEEDED for item in self.outcomes)
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def failed(self) -> int:
|
|
400
|
+
return sum(item.status is OutcomeStatus.FAILED for item in self.outcomes)
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def skipped(self) -> int:
|
|
404
|
+
return sum(item.status is OutcomeStatus.SKIPPED for item in self.outcomes)
|
cu_cli_core/defaults.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Client-injected CU service-default operations."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from .errors import NotFoundError, ValidationError
|
|
11
|
+
|
|
12
|
+
PREFERRED_EMBEDDING_MODEL = "text-embedding-3-large"
|
|
13
|
+
COMPLETION_MODEL_PREFERENCE = ("gpt-5.2",)
|
|
14
|
+
PREBUILT_COMPLETION_KEY = "prebuilt-analyzer-completion"
|
|
15
|
+
PREBUILT_COMPLETION_MINI_KEY = "prebuilt-analyzer-completion-mini"
|
|
16
|
+
PREBUILT_EMBEDDING_KEY = "prebuilt-analyzer-embedding"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def with_prebuilt_default_mappings(
|
|
20
|
+
model_deployments: Mapping[str, str],
|
|
21
|
+
) -> dict[str, str]:
|
|
22
|
+
"""Return model mappings enriched with the aliases required by prebuilt analyzers."""
|
|
23
|
+
|
|
24
|
+
out = {str(key): str(value) for key, value in model_deployments.items()}
|
|
25
|
+
model_names = list(out)
|
|
26
|
+
|
|
27
|
+
def is_alias(name: str) -> bool:
|
|
28
|
+
return name.startswith("prebuilt-analyzer-")
|
|
29
|
+
|
|
30
|
+
completion_model = next(
|
|
31
|
+
(model for model in COMPLETION_MODEL_PREFERENCE if model in out),
|
|
32
|
+
None,
|
|
33
|
+
)
|
|
34
|
+
if completion_model is None:
|
|
35
|
+
completion_model = next(
|
|
36
|
+
(
|
|
37
|
+
name
|
|
38
|
+
for name in model_names
|
|
39
|
+
if not is_alias(name)
|
|
40
|
+
and not name.startswith("text-embedding-")
|
|
41
|
+
and not name.endswith("-mini")
|
|
42
|
+
),
|
|
43
|
+
None,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
mini_model = next(
|
|
47
|
+
(
|
|
48
|
+
model
|
|
49
|
+
for model in ("gpt-5.2-mini", "gpt-4.1-mini", "gpt-4o-mini")
|
|
50
|
+
if model in out
|
|
51
|
+
),
|
|
52
|
+
None,
|
|
53
|
+
)
|
|
54
|
+
if mini_model is None:
|
|
55
|
+
mini_model = next(
|
|
56
|
+
(name for name in model_names if not is_alias(name) and name.endswith("-mini")),
|
|
57
|
+
None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
embedding_model = next(
|
|
61
|
+
(
|
|
62
|
+
name
|
|
63
|
+
for name in (PREFERRED_EMBEDDING_MODEL, *sorted(model_names))
|
|
64
|
+
if name in out and name.startswith("text-embedding-")
|
|
65
|
+
),
|
|
66
|
+
None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if completion_model is None and mini_model is not None:
|
|
70
|
+
completion_model = mini_model
|
|
71
|
+
if completion_model is not None:
|
|
72
|
+
out[PREBUILT_COMPLETION_KEY] = out[completion_model]
|
|
73
|
+
out[PREBUILT_COMPLETION_MINI_KEY] = (
|
|
74
|
+
out[mini_model] if mini_model else out[completion_model]
|
|
75
|
+
)
|
|
76
|
+
if embedding_model is not None:
|
|
77
|
+
out[PREBUILT_EMBEDDING_KEY] = out[embedding_model]
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_defaults_not_set(exc: Exception) -> bool:
|
|
82
|
+
"""Return whether a service exception means defaults have not been configured."""
|
|
83
|
+
|
|
84
|
+
message = getattr(exc, "message", None) or str(exc)
|
|
85
|
+
return "DefaultsNotSet" in message or "Defaults have not yet been set" in message
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_defaults(client: Any) -> Any:
|
|
89
|
+
"""Return service defaults from an injected client."""
|
|
90
|
+
|
|
91
|
+
from azure.core.exceptions import HttpResponseError
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
return client.get_defaults()
|
|
95
|
+
except HttpResponseError as exc:
|
|
96
|
+
if is_defaults_not_set(exc):
|
|
97
|
+
raise NotFoundError(
|
|
98
|
+
"defaults are not set yet on this resource.",
|
|
99
|
+
hint="Configure model deployments, then run defaults set.",
|
|
100
|
+
) from exc
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_model_deployments(defaults_obj: Any) -> dict[str, str]:
|
|
105
|
+
"""Return a defaults object's model-deployment mapping as strings."""
|
|
106
|
+
|
|
107
|
+
mapped = getattr(defaults_obj, "model_deployments", None) or {}
|
|
108
|
+
return {str(key): str(value) for key, value in mapped.items()}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_model_kv(values: Sequence[str]) -> dict[str, str]:
|
|
112
|
+
"""Parse repeatable ``MODEL=DEPLOYMENT`` values."""
|
|
113
|
+
|
|
114
|
+
parsed: dict[str, str] = {}
|
|
115
|
+
for raw in values:
|
|
116
|
+
if "=" not in raw:
|
|
117
|
+
raise ValidationError(
|
|
118
|
+
f"invalid --model mapping '{raw}'.",
|
|
119
|
+
hint="use --model MODEL=DEPLOYMENT (repeatable).",
|
|
120
|
+
)
|
|
121
|
+
model, deployment = (part.strip() for part in raw.split("=", 1))
|
|
122
|
+
if not model or not deployment:
|
|
123
|
+
raise ValidationError(
|
|
124
|
+
f"invalid --model mapping '{raw}'.",
|
|
125
|
+
hint="use --model MODEL=DEPLOYMENT (repeatable).",
|
|
126
|
+
)
|
|
127
|
+
parsed[model] = deployment
|
|
128
|
+
return parsed
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def apply_defaults(
|
|
132
|
+
client: Any,
|
|
133
|
+
desired: Mapping[str, str],
|
|
134
|
+
*,
|
|
135
|
+
replace: bool,
|
|
136
|
+
) -> tuple[Any, dict[str, str]]:
|
|
137
|
+
"""Merge or replace service defaults and return the result and final mapping."""
|
|
138
|
+
|
|
139
|
+
existing: dict[str, str] = {}
|
|
140
|
+
if not replace:
|
|
141
|
+
from azure.core.exceptions import HttpResponseError
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
existing = extract_model_deployments(client.get_defaults())
|
|
145
|
+
except HttpResponseError as exc:
|
|
146
|
+
if not is_defaults_not_set(exc):
|
|
147
|
+
raise
|
|
148
|
+
merged = dict(desired) if replace else {**existing, **desired}
|
|
149
|
+
merged = with_prebuilt_default_mappings(merged)
|
|
150
|
+
return client.update_defaults(model_deployments=merged), merged
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
"""Static environment-variable metadata and redacted process inspection."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import os
|
|
10
|
+
from typing import Mapping
|
|
11
|
+
|
|
12
|
+
REDACTED_VALUE = "********"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class EnvironmentVariableSpec:
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
accepted_values: str
|
|
20
|
+
default: str
|
|
21
|
+
scope: str
|
|
22
|
+
precedence: str
|
|
23
|
+
sensitive: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ENVIRONMENT_VARIABLES = (
|
|
27
|
+
EnvironmentVariableSpec(
|
|
28
|
+
name="CU_ENDPOINT",
|
|
29
|
+
description="Microsoft Foundry resource endpoint for Content Understanding.",
|
|
30
|
+
accepted_values="HTTPS URL",
|
|
31
|
+
default="not set",
|
|
32
|
+
scope="service commands",
|
|
33
|
+
precedence="Overrides the endpoint in the selected CU CLI profile.",
|
|
34
|
+
),
|
|
35
|
+
EnvironmentVariableSpec(
|
|
36
|
+
name="CU_API_KEY",
|
|
37
|
+
description="API key used for key authentication.",
|
|
38
|
+
accepted_values="secret string",
|
|
39
|
+
default="not set",
|
|
40
|
+
scope="authentication",
|
|
41
|
+
precedence="Overrides the selected CU CLI profile and implies key authentication.",
|
|
42
|
+
sensitive=True,
|
|
43
|
+
),
|
|
44
|
+
EnvironmentVariableSpec(
|
|
45
|
+
name="CU_AUTH_MODE",
|
|
46
|
+
description="Authentication mode.",
|
|
47
|
+
accepted_values="login | key",
|
|
48
|
+
default="login",
|
|
49
|
+
scope="authentication",
|
|
50
|
+
precedence="Overrides authentication in the selected CU CLI profile.",
|
|
51
|
+
),
|
|
52
|
+
EnvironmentVariableSpec(
|
|
53
|
+
name="CU_API_VERSION",
|
|
54
|
+
description="Content Understanding API version.",
|
|
55
|
+
accepted_values="supported API version",
|
|
56
|
+
default="2025-11-01",
|
|
57
|
+
scope="service commands",
|
|
58
|
+
precedence="Overrides the API version in the selected CU CLI profile.",
|
|
59
|
+
),
|
|
60
|
+
EnvironmentVariableSpec(
|
|
61
|
+
name="CU_TELEMETRY",
|
|
62
|
+
description="Controls the cu-cli User-Agent adoption marker.",
|
|
63
|
+
accepted_values="off | 0 | false | no to disable",
|
|
64
|
+
default="on",
|
|
65
|
+
scope="service commands",
|
|
66
|
+
precedence="Controls CLI telemetry behavior directly.",
|
|
67
|
+
),
|
|
68
|
+
EnvironmentVariableSpec(
|
|
69
|
+
name="CU_NO_UPDATE_CHECK",
|
|
70
|
+
description="Disables the daily package update check.",
|
|
71
|
+
accepted_values="1 | true | yes | on to disable",
|
|
72
|
+
default="off",
|
|
73
|
+
scope="update check",
|
|
74
|
+
precedence="Controls update-check behavior directly.",
|
|
75
|
+
),
|
|
76
|
+
EnvironmentVariableSpec(
|
|
77
|
+
name="CU_ON_EXISTS",
|
|
78
|
+
description="Chooses how analyze handles an existing result file.",
|
|
79
|
+
accepted_values="error | skip | reanalyze",
|
|
80
|
+
default="error",
|
|
81
|
+
scope="analyze",
|
|
82
|
+
precedence="Used when --on-existing is not specified.",
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def list_set_environment_variables(
|
|
88
|
+
environ: Mapping[str, str] | None = None,
|
|
89
|
+
) -> list[dict[str, object]]:
|
|
90
|
+
"""Return recognized variables currently set, redacting sensitive values."""
|
|
91
|
+
|
|
92
|
+
values = os.environ if environ is None else environ
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
"name": spec.name,
|
|
96
|
+
"value": REDACTED_VALUE if spec.sensitive else values[spec.name],
|
|
97
|
+
"scope": spec.scope,
|
|
98
|
+
"sensitive": spec.sensitive,
|
|
99
|
+
}
|
|
100
|
+
for spec in ENVIRONMENT_VARIABLES
|
|
101
|
+
if spec.name in values
|
|
102
|
+
]
|