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.
@@ -0,0 +1,604 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Shared CU profile storage and resolution over Azure CLI configuration."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import configparser
9
+ from contextlib import contextmanager
10
+ import copy
11
+ from dataclasses import dataclass, field
12
+ import hashlib
13
+ from io import StringIO
14
+ import os
15
+ from pathlib import Path
16
+ import re
17
+ import sys
18
+ import tempfile
19
+ from typing import Iterator, Mapping
20
+
21
+ from .errors import ConflictError, LocalIOError, NotFoundError, ValidationError
22
+ from .service_options import DEFAULT_API_VERSION
23
+
24
+ AZURE_CONFIG_DIR_ENV = "AZURE_CONFIG_DIR"
25
+ CU_SECTION = "cu"
26
+ DEFAULT_PROFILE_NAME = "default"
27
+ ACTIVE_PROFILE_KEY = "active_profile"
28
+ PROFILE_MARKER = "_created"
29
+ RESERVED_PROFILE_NAMES = frozenset({DEFAULT_PROFILE_NAME, "model_deployments"})
30
+ KNOWN_PROFILE_KEYS = {
31
+ "endpoint",
32
+ "auth_mode",
33
+ "api_key",
34
+ "api_version",
35
+ "default_analyzer",
36
+ }
37
+ MODEL_DEPLOYMENTS_PREFIX = "model_deployments."
38
+ MODEL_ENV_OVERRIDES: Mapping[str, str] = {
39
+ "gpt-5.2": "GPT_5_2_DEPLOYMENT",
40
+ "gpt-4.1": "GPT_4_1_DEPLOYMENT",
41
+ "text-embedding-3-large": "TEXT_EMBEDDING_3_LARGE_DEPLOYMENT",
42
+ }
43
+ PROFILE_NAME_HINT = (
44
+ "use 1-64 ASCII letters or numbers, with hyphens (-) or underscores (_) "
45
+ "only between characters; for example 'dev', 'West_US2', or 'test-01'. "
46
+ "'default' and 'model_deployments' are reserved."
47
+ )
48
+ _PROFILE_NAME_PATTERN = re.compile(
49
+ r"^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,62}[A-Za-z0-9])?$"
50
+ )
51
+ _MODEL_NAME_PATTERN = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$")
52
+ _SECTION_PATTERN = re.compile(
53
+ r"^[ \t]*\[(?P<name>[^\]\r\n]+)\][ \t]*(?:[;#].*)?(?:\r?\n|$)",
54
+ re.MULTILINE,
55
+ )
56
+
57
+
58
+ class _CaseSensitiveConfigParser(configparser.ConfigParser):
59
+ def optionxform(self, optionstr: str) -> str:
60
+ return optionstr
61
+
62
+
63
+ def azure_config_path() -> Path:
64
+ """Return the active Azure CLI configuration file path."""
65
+
66
+ configured = os.getenv(AZURE_CONFIG_DIR_ENV)
67
+ config_dir = Path(configured).expanduser() if configured else Path.home() / ".azure"
68
+ return config_dir / "config"
69
+
70
+
71
+ def normalize_profile_name(name: str) -> str:
72
+ """Validate a mutable named profile without changing its case."""
73
+
74
+ if (
75
+ _PROFILE_NAME_PATTERN.fullmatch(name) is None
76
+ or name.casefold() in RESERVED_PROFILE_NAMES
77
+ ):
78
+ raise ValidationError(f"invalid profile name '{name}'.", hint=PROFILE_NAME_HINT)
79
+ return name
80
+
81
+
82
+ def validate_profile_name(name: str) -> str:
83
+ if name == DEFAULT_PROFILE_NAME:
84
+ return name
85
+ return normalize_profile_name(name)
86
+
87
+
88
+ def is_valid_profile_name(name: str, *, allow_default: bool = True) -> bool:
89
+ if _PROFILE_NAME_PATTERN.fullmatch(name) is None:
90
+ return False
91
+ folded = name.casefold()
92
+ if folded == DEFAULT_PROFILE_NAME:
93
+ return allow_default and name == DEFAULT_PROFILE_NAME
94
+ return folded not in RESERVED_PROFILE_NAMES
95
+
96
+
97
+ def validate_profile_key(key: str) -> str:
98
+ if key.startswith(MODEL_DEPLOYMENTS_PREFIX):
99
+ model = key.removeprefix(MODEL_DEPLOYMENTS_PREFIX)
100
+ if _MODEL_NAME_PATTERN.fullmatch(model) is not None:
101
+ return key
102
+ raise ValidationError(
103
+ f"invalid model deployment key '{key}'.",
104
+ hint=(
105
+ "use model_deployments.<model> with letters, numbers, dots, "
106
+ "hyphens, or underscores; for example model_deployments.gpt-5.2."
107
+ ),
108
+ )
109
+ if key not in KNOWN_PROFILE_KEYS:
110
+ known = ", ".join(sorted(KNOWN_PROFILE_KEYS))
111
+ raise ValidationError(
112
+ f"unknown profile key '{key}'.",
113
+ hint=f"known keys: {known}, or model_deployments.<model>.",
114
+ )
115
+ return key
116
+
117
+
118
+ def validate_profile_value(key: str, value: str) -> str:
119
+ cleaned = value.strip()
120
+ if not cleaned:
121
+ raise ValidationError(f"value for '{key}' must not be empty.")
122
+ if "\n" in cleaned or "\r" in cleaned:
123
+ raise ValidationError(f"value for '{key}' must be a single line.")
124
+ if key == "auth_mode" and cleaned not in {"login", "key"}:
125
+ raise ValidationError("auth_mode must be 'login' or 'key'.")
126
+ return cleaned
127
+
128
+
129
+ def _read_config(path: Path) -> tuple[str, dict[str, str], str | None]:
130
+ try:
131
+ raw = path.read_bytes()
132
+ except FileNotFoundError:
133
+ return "", {}, None
134
+ except OSError as exc:
135
+ raise LocalIOError(
136
+ f"could not read Azure CLI configuration '{path}': {exc}.",
137
+ hint="check the file permissions and try again.",
138
+ ) from exc
139
+ try:
140
+ text = raw.decode("utf-8")
141
+ except UnicodeDecodeError as exc:
142
+ raise ValidationError(
143
+ f"Azure CLI configuration '{path}' is not valid UTF-8.",
144
+ hint="fix or move the file before changing CU profiles.",
145
+ ) from exc
146
+
147
+ parser = _CaseSensitiveConfigParser(interpolation=None, strict=True)
148
+ try:
149
+ parser.read_string(text)
150
+ except configparser.Error as exc:
151
+ raise ValidationError(
152
+ f"Azure CLI configuration '{path}' is not valid INI: {exc}.",
153
+ hint="fix the configuration before changing CU profiles.",
154
+ ) from exc
155
+ values = dict(parser.items(CU_SECTION)) if parser.has_section(CU_SECTION) else {}
156
+ return text, values, hashlib.sha256(raw).hexdigest()
157
+
158
+
159
+ @contextmanager
160
+ def _exclusive_config_lock(path: Path) -> Iterator[None]:
161
+ lock_path = path.with_name(f"{path.name}.lock")
162
+ with open(lock_path, "a+b") as lock_file:
163
+ if sys.platform == "win32":
164
+ import msvcrt
165
+
166
+ lock_file.seek(0, os.SEEK_END)
167
+ if lock_file.tell() == 0:
168
+ lock_file.write(b"\0")
169
+ lock_file.flush()
170
+ lock_file.seek(0)
171
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
172
+ try:
173
+ yield
174
+ finally:
175
+ lock_file.seek(0)
176
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
177
+ else:
178
+ import fcntl
179
+
180
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
181
+ try:
182
+ yield
183
+ finally:
184
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
185
+
186
+
187
+ def _render_cu_section(values: Mapping[str, str], newline: str) -> str:
188
+ if not values:
189
+ return ""
190
+ parser = _CaseSensitiveConfigParser(interpolation=None)
191
+ parser.add_section(CU_SECTION)
192
+ for key in sorted(values):
193
+ parser.set(CU_SECTION, key, values[key])
194
+ stream = StringIO()
195
+ parser.write(stream, space_around_delimiters=True)
196
+ rendered = stream.getvalue()
197
+ return rendered.replace("\n", newline)
198
+
199
+
200
+ def _replace_cu_section(original: str, values: Mapping[str, str]) -> str:
201
+ newline = "\r\n" if "\r\n" in original else "\n"
202
+ matches = list(_SECTION_PATTERN.finditer(original))
203
+ cu_match_index = next(
204
+ (
205
+ index
206
+ for index, match in enumerate(matches)
207
+ if match.group("name").strip().casefold() == CU_SECTION
208
+ ),
209
+ None,
210
+ )
211
+ replacement = _render_cu_section(values, newline)
212
+ if cu_match_index is None:
213
+ if not replacement:
214
+ return original
215
+ separator = "" if not original else newline if original.endswith(("\n", "\r")) else newline * 2
216
+ return f"{original}{separator}{replacement}"
217
+
218
+ start = matches[cu_match_index].start()
219
+ end = (
220
+ matches[cu_match_index + 1].start()
221
+ if cu_match_index + 1 < len(matches)
222
+ else len(original)
223
+ )
224
+ suffix = original[end:]
225
+ if replacement and suffix and not replacement.endswith(("\n", "\r")):
226
+ replacement += newline
227
+ return f"{original[:start]}{replacement}{suffix}"
228
+
229
+
230
+ def _profile_prefix(name: str) -> str:
231
+ return f"{name}."
232
+
233
+
234
+ def _is_profile_setting(key: str) -> bool:
235
+ return key in KNOWN_PROFILE_KEYS or key.startswith(MODEL_DEPLOYMENTS_PREFIX)
236
+
237
+
238
+ @dataclass
239
+ class ProfileStore:
240
+ """Read and atomically update CU profiles in Azure CLI configuration."""
241
+
242
+ path: Path
243
+ values: dict[str, str] = field(default_factory=dict)
244
+ _source_text: str = field(default="", repr=False)
245
+ _source_digest: str | None = field(default=None, repr=False)
246
+
247
+ @classmethod
248
+ def load(cls, path: Path | None = None) -> "ProfileStore":
249
+ resolved_path = path or azure_config_path()
250
+ text, values, digest = _read_config(resolved_path)
251
+ store = cls(
252
+ path=resolved_path,
253
+ values=values,
254
+ _source_text=text,
255
+ _source_digest=digest,
256
+ )
257
+ store._validate()
258
+ return store
259
+
260
+ def _validate(self) -> None:
261
+ active = self.values.get(ACTIVE_PROFILE_KEY)
262
+ if active is not None:
263
+ validate_profile_name(active)
264
+ for key in self.values:
265
+ if key == ACTIVE_PROFILE_KEY:
266
+ continue
267
+ if _is_profile_setting(key):
268
+ validate_profile_key(key)
269
+ continue
270
+ name, separator, setting = key.partition(".")
271
+ if not separator:
272
+ continue
273
+ validate_profile_name(name)
274
+ if setting != PROFILE_MARKER:
275
+ validate_profile_key(setting)
276
+
277
+ def list_names(self) -> list[str]:
278
+ names = {DEFAULT_PROFILE_NAME}
279
+ for key in self.values:
280
+ name, separator, setting = key.partition(".")
281
+ if separator and (
282
+ setting == PROFILE_MARKER or _is_profile_setting(setting)
283
+ ):
284
+ names.add(name)
285
+ return sorted(names)
286
+
287
+ def has_name(self, name: str) -> bool:
288
+ if name == DEFAULT_PROFILE_NAME:
289
+ return True
290
+ prefix = _profile_prefix(name)
291
+ return any(key.startswith(prefix) for key in self.values)
292
+
293
+ def get_active_name(self) -> str:
294
+ active = self.values.get(ACTIVE_PROFILE_KEY, DEFAULT_PROFILE_NAME)
295
+ if not self.has_name(active):
296
+ raise NotFoundError(
297
+ f"active CU CLI profile '{active}' was not found.",
298
+ hint="run `cu profile set-active default` or select another listed profile.",
299
+ )
300
+ return active
301
+
302
+ def set_active_name(self, name: str) -> None:
303
+ target = validate_profile_name(name)
304
+ if not self.has_name(target):
305
+ raise NotFoundError(
306
+ f"profile '{target}' was not found.",
307
+ hint="run `cu profile list` to see available profiles.",
308
+ )
309
+ if target == DEFAULT_PROFILE_NAME:
310
+ self.values.pop(ACTIVE_PROFILE_KEY, None)
311
+ else:
312
+ self.values[ACTIVE_PROFILE_KEY] = target
313
+
314
+ def _base_profile(self) -> dict[str, str]:
315
+ return {
316
+ key: value
317
+ for key, value in self.values.items()
318
+ if _is_profile_setting(key)
319
+ }
320
+
321
+ def get_explicit_profile(self, name: str) -> dict[str, str]:
322
+ target = validate_profile_name(name)
323
+ prefix = _profile_prefix(target)
324
+ return {
325
+ key.removeprefix(prefix): value
326
+ for key, value in self.values.items()
327
+ if key.startswith(prefix)
328
+ and key.removeprefix(prefix) != PROFILE_MARKER
329
+ }
330
+
331
+ def get_profile(self, name: str | None = None) -> dict[str, object]:
332
+ target = self.get_active_name() if name is None else validate_profile_name(name)
333
+ if not self.has_name(target):
334
+ raise NotFoundError(
335
+ f"profile '{target}' was not found.",
336
+ hint="run `cu profile list` to see available profiles.",
337
+ )
338
+ flat = {**self._base_profile(), **self.get_explicit_profile(target)}
339
+ models = {
340
+ key.removeprefix(MODEL_DEPLOYMENTS_PREFIX): value
341
+ for key, value in flat.items()
342
+ if key.startswith(MODEL_DEPLOYMENTS_PREFIX)
343
+ }
344
+ profile: dict[str, object] = {
345
+ key: value for key, value in flat.items() if key in KNOWN_PROFILE_KEYS
346
+ }
347
+ if models:
348
+ profile["model_deployments"] = models
349
+ return profile
350
+
351
+ def create_name(self, name: str) -> None:
352
+ target = normalize_profile_name(name)
353
+ if self.has_name(target):
354
+ raise ConflictError(f"profile '{target}' already exists.")
355
+ self.values[f"{target}.{PROFILE_MARKER}"] = "true"
356
+
357
+ def get(self, key: str, *, name: str | None = None) -> str | None:
358
+ setting = validate_profile_key(key)
359
+ target = self.get_active_name() if name is None else validate_profile_name(name)
360
+ if not self.has_name(target):
361
+ raise NotFoundError(f"profile '{target}' was not found.")
362
+ return self.values.get(f"{target}.{setting}") or self.values.get(setting)
363
+
364
+ def set(self, key: str, value: str, *, name: str | None = None) -> None:
365
+ setting = validate_profile_key(key)
366
+ cleaned = validate_profile_value(setting, value)
367
+ target = self.get_active_name() if name is None else validate_profile_name(name)
368
+ if not self.has_name(target):
369
+ raise NotFoundError(
370
+ f"profile '{target}' was not found.",
371
+ hint=f"create it with `cu profile create {target}` first.",
372
+ )
373
+ self.values[f"{target}.{setting}"] = cleaned
374
+ if setting == "api_key":
375
+ self.values[f"{target}.auth_mode"] = "key"
376
+
377
+ def unset(self, key: str, *, name: str | None = None) -> bool:
378
+ setting = validate_profile_key(key)
379
+ target = self.get_active_name() if name is None else validate_profile_name(name)
380
+ if not self.has_name(target):
381
+ raise NotFoundError(f"profile '{target}' was not found.")
382
+ removed = self.values.pop(f"{target}.{setting}", None) is not None
383
+ if setting == "api_key":
384
+ self.values[f"{target}.auth_mode"] = "login"
385
+ return removed
386
+
387
+ def replace_model_deployments(
388
+ self,
389
+ model_deployments: Mapping[str, str],
390
+ *,
391
+ name: str | None = None,
392
+ ) -> None:
393
+ target = self.get_active_name() if name is None else validate_profile_name(name)
394
+ prefix = f"{target}.{MODEL_DEPLOYMENTS_PREFIX}"
395
+ for key in tuple(self.values):
396
+ if key.startswith(prefix):
397
+ del self.values[key]
398
+ for model, deployment in model_deployments.items():
399
+ self.set(
400
+ f"{MODEL_DEPLOYMENTS_PREFIX}{model}",
401
+ str(deployment),
402
+ name=target,
403
+ )
404
+
405
+ def has_explicit_model_deployments(self, name: str | None = None) -> bool:
406
+ target = self.get_active_name() if name is None else validate_profile_name(name)
407
+ prefix = f"{target}.{MODEL_DEPLOYMENTS_PREFIX}"
408
+ return any(key.startswith(prefix) for key in self.values)
409
+
410
+ def copy_name(self, source: str, destination: str) -> None:
411
+ source_name = validate_profile_name(source)
412
+ destination_name = normalize_profile_name(destination)
413
+ if not self.has_name(source_name):
414
+ raise NotFoundError(f"profile '{source_name}' was not found.")
415
+ if self.has_name(destination_name):
416
+ raise ConflictError(f"profile '{destination_name}' already exists.")
417
+ self.create_name(destination_name)
418
+ for key, value in self.get_explicit_profile(source_name).items():
419
+ self.values[f"{destination_name}.{key}"] = copy.deepcopy(value)
420
+
421
+ def rename_name(self, source: str, destination: str) -> None:
422
+ source_name = normalize_profile_name(source)
423
+ destination_name = normalize_profile_name(destination)
424
+ if not self.has_name(source_name):
425
+ raise NotFoundError(f"profile '{source_name}' was not found.")
426
+ if self.has_name(destination_name):
427
+ raise ConflictError(f"profile '{destination_name}' already exists.")
428
+ source_prefix = _profile_prefix(source_name)
429
+ moved = {
430
+ f"{destination_name}.{key.removeprefix(source_prefix)}": value
431
+ for key, value in self.values.items()
432
+ if key.startswith(source_prefix)
433
+ }
434
+ for key in tuple(self.values):
435
+ if key.startswith(source_prefix):
436
+ del self.values[key]
437
+ self.values.update(moved)
438
+ if self.values.get(ACTIVE_PROFILE_KEY) == source_name:
439
+ self.values[ACTIVE_PROFILE_KEY] = destination_name
440
+
441
+ def delete_name(self, name: str) -> None:
442
+ target = normalize_profile_name(name)
443
+ if not self.has_name(target):
444
+ raise NotFoundError(f"profile '{target}' was not found.")
445
+ if self.get_active_name() == target:
446
+ raise ConflictError(
447
+ f"cannot delete active CU CLI profile '{target}'.",
448
+ hint="activate another profile before deleting it.",
449
+ )
450
+ prefix = _profile_prefix(target)
451
+ for key in tuple(self.values):
452
+ if key.startswith(prefix):
453
+ del self.values[key]
454
+
455
+ def save(self) -> Path:
456
+ self.path.parent.mkdir(parents=True, exist_ok=True)
457
+ with _exclusive_config_lock(self.path):
458
+ try:
459
+ current_raw = self.path.read_bytes()
460
+ except FileNotFoundError:
461
+ current_digest = None
462
+ except OSError as exc:
463
+ raise LocalIOError(
464
+ f"could not verify Azure CLI configuration '{self.path}': {exc}."
465
+ ) from exc
466
+ else:
467
+ current_digest = hashlib.sha256(current_raw).hexdigest()
468
+ if current_digest != self._source_digest:
469
+ raise ConflictError(
470
+ f"Azure CLI configuration '{self.path}' changed after it was loaded.",
471
+ hint="reload the profile and retry the command.",
472
+ )
473
+
474
+ updated = _replace_cu_section(self._source_text, self.values)
475
+ encoded = updated.encode("utf-8")
476
+ fd, temporary_name = tempfile.mkstemp(
477
+ prefix=f".{self.path.name}.",
478
+ dir=self.path.parent,
479
+ )
480
+ try:
481
+ with os.fdopen(fd, "wb") as handle:
482
+ handle.write(encoded)
483
+ handle.flush()
484
+ os.fsync(handle.fileno())
485
+ os.replace(temporary_name, self.path)
486
+ self.path.chmod(0o600)
487
+ except Exception:
488
+ Path(temporary_name).unlink(missing_ok=True)
489
+ raise
490
+ self._source_text = updated
491
+ self._source_digest = hashlib.sha256(encoded).hexdigest()
492
+ return self.path
493
+
494
+
495
+ @dataclass
496
+ class Profile:
497
+ """Effective CU settings after built-in, saved-profile, and environment precedence."""
498
+
499
+ endpoint: str | None = None
500
+ auth_mode: str = "login"
501
+ api_key: str | None = None
502
+ api_version: str = DEFAULT_API_VERSION
503
+ default_analyzer: str | None = None
504
+ model_deployments: dict[str, str] = field(default_factory=dict)
505
+ profile_name: str = DEFAULT_PROFILE_NAME
506
+ path: Path = field(default_factory=azure_config_path)
507
+
508
+ @classmethod
509
+ def load_saved(
510
+ cls,
511
+ *,
512
+ profile_name: str | None = None,
513
+ path: Path | None = None,
514
+ ) -> "Profile":
515
+ store = ProfileStore.load(path)
516
+ selected = store.get_active_name() if profile_name is None else validate_profile_name(profile_name)
517
+ profile = cls(profile_name=selected, path=store.path)
518
+ profile._overlay(store.get_profile(selected), source=f"profile '{selected}'")
519
+ return profile
520
+
521
+ @classmethod
522
+ def load(
523
+ cls,
524
+ *,
525
+ profile_name: str | None = None,
526
+ path: Path | None = None,
527
+ ) -> "Profile":
528
+ profile = cls.load_saved(profile_name=profile_name, path=path)
529
+ profile._apply_environment()
530
+ return profile
531
+
532
+ def _apply_environment(self) -> None:
533
+ endpoint = os.getenv("CU_ENDPOINT") or os.getenv("CONTENTUNDERSTANDING_ENDPOINT")
534
+ if endpoint:
535
+ self.endpoint = endpoint
536
+ auth_mode = os.getenv("CU_AUTH_MODE")
537
+ if auth_mode:
538
+ self.auth_mode = validate_profile_value("auth_mode", auth_mode)
539
+ api_key = os.getenv("CU_API_KEY") or os.getenv("CONTENTUNDERSTANDING_KEY")
540
+ if api_key:
541
+ self.api_key = api_key
542
+ self.auth_mode = "key"
543
+ api_version = os.getenv("CU_API_VERSION")
544
+ if api_version:
545
+ self.api_version = api_version
546
+ for model, environment_name in MODEL_ENV_OVERRIDES.items():
547
+ deployment = os.getenv(environment_name)
548
+ if deployment:
549
+ self.model_deployments[model] = deployment
550
+
551
+ @staticmethod
552
+ def _optional_string(
553
+ values: Mapping[str, object],
554
+ key: str,
555
+ *,
556
+ source: str,
557
+ ) -> str | None:
558
+ value = values.get(key)
559
+ if value is None:
560
+ return None
561
+ if not isinstance(value, str) or not value.strip():
562
+ raise ValidationError(
563
+ f"{source} has an invalid '{key}' value; expected a non-empty string."
564
+ )
565
+ return value
566
+
567
+ def _overlay(self, values: Mapping[str, object], *, source: str) -> None:
568
+ endpoint = self._optional_string(values, "endpoint", source=source)
569
+ if endpoint is not None:
570
+ self.endpoint = endpoint
571
+ auth_mode = self._optional_string(values, "auth_mode", source=source)
572
+ if auth_mode is not None:
573
+ self.auth_mode = validate_profile_value("auth_mode", auth_mode)
574
+ api_key = self._optional_string(values, "api_key", source=source)
575
+ if api_key is not None:
576
+ self.api_key = api_key
577
+ if auth_mode is None:
578
+ self.auth_mode = "key"
579
+ api_version = self._optional_string(values, "api_version", source=source)
580
+ if api_version is not None:
581
+ self.api_version = api_version
582
+ default_analyzer = self._optional_string(values, "default_analyzer", source=source)
583
+ if default_analyzer is not None:
584
+ self.default_analyzer = default_analyzer
585
+ models = values.get("model_deployments")
586
+ if models is not None:
587
+ if not isinstance(models, Mapping):
588
+ raise ValidationError(
589
+ f"{source} has invalid model_deployments; expected a mapping."
590
+ )
591
+ self.model_deployments.update(
592
+ {str(key): str(value) for key, value in models.items()}
593
+ )
594
+
595
+ def to_public_dict(self) -> dict[str, object]:
596
+ return {
597
+ "profile": self.profile_name,
598
+ "endpoint": self.endpoint,
599
+ "auth_mode": self.auth_mode,
600
+ "api_key": "***redacted***" if self.api_key else None,
601
+ "api_version": self.api_version,
602
+ "default_analyzer": self.default_analyzer,
603
+ "model_deployments": dict(self.model_deployments),
604
+ }
cu_cli_core/py.typed ADDED
File without changes