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,312 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Local, structural schema validation against the bundled rules for an
5
+ api-version.
6
+
7
+ Deterministic and offline — **no service call**. ``validate`` produces detailed,
8
+ actionable messages: on a bad field ``type`` it names the field and lists the
9
+ accepted types, and returns exit code ``2`` on any error (so agents can branch).
10
+
11
+ The accepted types/methods are shared by both MVP api-versions; the
12
+ ``api_version`` argument is threaded through so future versions can diverge
13
+ without changing call sites.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ from dataclasses import dataclass, field
21
+ from importlib import resources as ir
22
+ from typing import Any, List, Optional, Tuple
23
+
24
+ from .service_options import DEFAULT_API_VERSION
25
+
26
+ # Accepted field types/methods come from the bundled CU OpenAPI spec (the service
27
+ # contract) so they never drift from the API. A hardcoded fallback keeps the
28
+ # validator working if the bundled spec is unavailable.
29
+ _FALLBACK_TYPES = ["array", "boolean", "date", "integer", "json", "number",
30
+ "object", "string", "time"]
31
+ _FALLBACK_METHODS = ["classify", "extract", "generate"]
32
+
33
+
34
+ def _spec_enums(api_version: str) -> Tuple[List[str], List[str]]:
35
+ try:
36
+ text = (ir.files("cu_cli_core")
37
+ .joinpath(f"resources/openapi/{api_version}/ContentUnderstanding.json")
38
+ .read_text(encoding="utf-8"))
39
+ defs = json.loads(text).get("definitions", {})
40
+ types = defs.get("ContentFieldType", {}).get("enum")
41
+ methods = defs.get("GenerationMethod", {}).get("enum")
42
+ return (sorted(types) if isinstance(types, list) and types else _FALLBACK_TYPES,
43
+ sorted(methods) if isinstance(methods, list) and methods else _FALLBACK_METHODS)
44
+ except Exception:
45
+ return _FALLBACK_TYPES, _FALLBACK_METHODS
46
+
47
+
48
+ ALLOWED_TYPES, ALLOWED_METHODS = _spec_enums(DEFAULT_API_VERSION)
49
+
50
+ _CUSTOM_ANALYZER_ID_RE = re.compile(r"^[a-zA-Z0-9_]{1,64}$")
51
+ _BASE_ANALYZER_ID_RE = re.compile(r"^[a-zA-Z0-9._-]{1,64}$")
52
+ _MAX_NESTING = 4
53
+ _MIN_DESCRIPTION_LEN = 15
54
+
55
+
56
+ @dataclass
57
+ class Finding:
58
+ path: str
59
+ msg: str
60
+
61
+
62
+ @dataclass
63
+ class ValidationResult:
64
+ ok: bool
65
+ errors: List[Finding] = field(default_factory=list)
66
+ warnings: List[Finding] = field(default_factory=list)
67
+
68
+ def as_dict(self) -> dict:
69
+ return {
70
+ "ok": self.ok,
71
+ "errors": [{"path": e.path, "msg": e.msg} for e in self.errors],
72
+ "warnings": [{"path": w.path, "msg": w.msg} for w in self.warnings],
73
+ }
74
+
75
+
76
+ def _types_list() -> str:
77
+ return "[" + ", ".join(ALLOWED_TYPES) + "]"
78
+
79
+
80
+ def custom_analyzer_id_error(value: Any) -> str | None:
81
+ """Return the user-facing validation error for a custom analyzer ID."""
82
+ if isinstance(value, str) and _CUSTOM_ANALYZER_ID_RE.fullmatch(value):
83
+ return None
84
+ return (
85
+ "must contain 1-64 ASCII letters, numbers, or underscores. "
86
+ "Hyphens are reserved for service-provided prebuilt analyzer IDs "
87
+ "(for example, 'prebuilt-invoice')."
88
+ )
89
+
90
+
91
+ def _validate_field(name: str, defn: Any, path: str, depth: int,
92
+ errors: List[Finding], warnings: List[Finding]) -> None:
93
+ if not isinstance(defn, dict):
94
+ errors.append(Finding(path, f"field '{name}' must be an object."))
95
+ return
96
+
97
+ if "$ref" in defn:
98
+ errors.append(Finding(
99
+ f"{path}.$ref",
100
+ "`$ref` is not currently supported in analyzer field schemas; "
101
+ "inline the field definition.",
102
+ ))
103
+ return
104
+
105
+ ftype = defn.get("type")
106
+
107
+ if ftype is None:
108
+ errors.append(Finding(
109
+ f"{path}.type",
110
+ "missing `type`; every field definition requires an explicit type.",
111
+ ))
112
+ elif ftype is not None and ftype not in ALLOWED_TYPES:
113
+ errors.append(Finding(
114
+ f"{path}.type",
115
+ f"must be one of {_types_list()}, got '{ftype}'.",
116
+ ))
117
+
118
+ desc = defn.get("description")
119
+ if desc is None or (isinstance(desc, str) and not desc.strip()):
120
+ warnings.append(Finding(f"{path}.description",
121
+ "no `description` — the model uses this as a per-field prompt."))
122
+ elif not isinstance(desc, str):
123
+ errors.append(Finding(f"{path}.description", "must be a string."))
124
+ elif len(desc.strip()) < _MIN_DESCRIPTION_LEN:
125
+ warnings.append(Finding(f"{path}.description",
126
+ f"description is very short ({len(desc.strip())} chars); be specific."))
127
+
128
+ method = defn.get("method")
129
+ if method is not None and method not in ALLOWED_METHODS:
130
+ errors.append(Finding(f"{path}.method",
131
+ f"must be one of [{', '.join(ALLOWED_METHODS)}], got '{method}'."))
132
+ if method == "classify":
133
+ enum = defn.get("enum")
134
+ if not isinstance(enum, list) or len({str(e) for e in enum}) < 2:
135
+ errors.append(Finding(f"{path}.enum",
136
+ "`classify` fields require `enum` with >=2 distinct values."))
137
+
138
+ if ftype == "array":
139
+ items = defn.get("items")
140
+ if not isinstance(items, dict):
141
+ errors.append(Finding(f"{path}.items", "`array` fields require an `items` object."))
142
+ else:
143
+ _depth_check(depth + 1, f"{path}.items", errors, warnings)
144
+ _validate_field(f"{name}[]", items, f"{path}.items", depth + 1, errors, warnings)
145
+
146
+ if ftype == "object":
147
+ props = defn.get("properties")
148
+ if not isinstance(props, dict) or not props:
149
+ errors.append(Finding(
150
+ f"{path}.properties",
151
+ "`object` fields require a non-empty `properties` map.",
152
+ ))
153
+ else:
154
+ _depth_check(depth + 1, f"{path}.properties", errors, warnings)
155
+ for sub_name, sub_def in props.items():
156
+ _validate_field(sub_name, sub_def, f"{path}.properties.{sub_name}",
157
+ depth + 1, errors, warnings)
158
+
159
+ esc = defn.get("estimateSourceAndConfidence")
160
+ if esc is not None and not isinstance(esc, bool):
161
+ errors.append(Finding(f"{path}.estimateSourceAndConfidence", "must be a boolean."))
162
+
163
+
164
+ def _depth_check(depth: int, path: str, errors: List[Finding], warnings: List[Finding]) -> None:
165
+ if depth > _MAX_NESTING:
166
+ warnings.append(Finding(path,
167
+ f"nesting depth {depth}; accuracy drops past depth "
168
+ f"{_MAX_NESTING}. Consider flattening."))
169
+
170
+
171
+ def validate_schema(body: Any, *, api_version: Optional[str] = None) -> ValidationResult:
172
+ errors: List[Finding] = []
173
+ warnings: List[Finding] = []
174
+
175
+ if not isinstance(body, dict):
176
+ errors.append(Finding("$", "schema root must be a JSON object."))
177
+ return ValidationResult(ok=False, errors=errors, warnings=warnings)
178
+
179
+ aid = body.get("analyzerId") or body.get("analyzer_id")
180
+ if aid is not None:
181
+ aid_error = custom_analyzer_id_error(aid)
182
+ if aid_error:
183
+ errors.append(Finding("analyzerId", aid_error))
184
+
185
+ base = body.get("baseAnalyzerId") or body.get("base_analyzer_id")
186
+ if base is not None and (not isinstance(base, str) or not _BASE_ANALYZER_ID_RE.match(base)):
187
+ errors.append(Finding("baseAnalyzerId", "must match ^[a-zA-Z0-9._-]{1,64}$."))
188
+ elif base is None:
189
+ warnings.append(Finding("baseAnalyzerId",
190
+ "no `baseAnalyzerId`; recommend `prebuilt-document`."))
191
+
192
+ pinned = body.get("apiVersion")
193
+ if pinned is not None and api_version is not None and pinned != api_version:
194
+ # Callers pass the already-reconciled api_version; a residual mismatch
195
+ # here is surfaced so the schema and resolved version never diverge.
196
+ errors.append(Finding("apiVersion",
197
+ f"schema pins '{pinned}' but resolved api-version is '{api_version}'."))
198
+
199
+ models = body.get("models")
200
+ if models is not None and not isinstance(models, dict):
201
+ errors.append(Finding("models", "must be an object mapping role -> model name."))
202
+
203
+ has_content_categories = False
204
+ cfg = body.get("config")
205
+ if cfg is not None:
206
+ if not isinstance(cfg, dict):
207
+ errors.append(Finding("config", "must be an object."))
208
+ else:
209
+ for bool_key in ("enableSegment", "segmentPerPage", "estimateFieldSourceAndConfidence"):
210
+ val = cfg.get(bool_key)
211
+ if val is not None and not isinstance(val, bool):
212
+ errors.append(Finding(f"config.{bool_key}", "must be a boolean."))
213
+
214
+ cats = cfg.get("contentCategories")
215
+ if cats is not None:
216
+ if not isinstance(cats, dict) or not cats:
217
+ errors.append(Finding("config.contentCategories",
218
+ "must be a non-empty object when provided."))
219
+ else:
220
+ has_content_categories = True
221
+ for cat_name, cat_def in cats.items():
222
+ if not isinstance(cat_def, dict):
223
+ errors.append(Finding(
224
+ f"config.contentCategories.{cat_name}",
225
+ "must be an object with category definition fields.",
226
+ ))
227
+ continue
228
+
229
+ desc = cat_def.get("description")
230
+ if desc is None or (isinstance(desc, str) and not desc.strip()):
231
+ warnings.append(Finding(
232
+ f"config.contentCategories.{cat_name}.description",
233
+ "no `description` — classification quality improves with clear category intent.",
234
+ ))
235
+ elif not isinstance(desc, str):
236
+ errors.append(Finding(
237
+ f"config.contentCategories.{cat_name}.description",
238
+ "must be a string.",
239
+ ))
240
+
241
+ analyzer_id = cat_def.get("analyzerId")
242
+ if analyzer_id is not None:
243
+ if not isinstance(analyzer_id, str) or not _BASE_ANALYZER_ID_RE.match(analyzer_id):
244
+ errors.append(Finding(
245
+ f"config.contentCategories.{cat_name}.analyzerId",
246
+ "must match ^[a-zA-Z0-9._-]{1,64}$.",
247
+ ))
248
+
249
+ fs = body.get("fieldSchema") or body.get("field_schema")
250
+ if fs is None:
251
+ if not has_content_categories:
252
+ warnings.append(Finding("fieldSchema", "no `fieldSchema`; layout/markdown only."))
253
+ elif not isinstance(fs, dict):
254
+ errors.append(Finding("fieldSchema", "must be an object."))
255
+ else:
256
+ fields = fs.get("fields")
257
+ if fields is None:
258
+ warnings.append(Finding("fieldSchema.fields", "no `fields`; layout only."))
259
+ elif not isinstance(fields, dict):
260
+ errors.append(Finding("fieldSchema.fields", "must be an object."))
261
+ elif fields:
262
+ models_map = models if isinstance(models, dict) else {}
263
+ if not models_map.get("completion"):
264
+ warnings.append(Finding("models.completion",
265
+ "field extraction needs a completion model, e.g. "
266
+ "`\"models\": {\"completion\": \"gpt-5.2\"}`."))
267
+ for name, defn in fields.items():
268
+ _validate_field(name, defn, f"fieldSchema.fields.{name}", 1, errors, warnings)
269
+
270
+ defs = fs.get("definitions")
271
+ if defs is not None:
272
+ if not isinstance(defs, dict):
273
+ errors.append(Finding("fieldSchema.definitions", "must be an object."))
274
+ else:
275
+ for name, defn in defs.items():
276
+ _validate_field(name, defn, f"fieldSchema.definitions.{name}", 1,
277
+ errors, warnings)
278
+
279
+ return ValidationResult(ok=not errors, errors=errors, warnings=warnings)
280
+
281
+
282
+ def schema_pinned_version(body: Any) -> Optional[str]:
283
+ if isinstance(body, dict):
284
+ v = body.get("apiVersion")
285
+ return v if isinstance(v, str) else None
286
+ return None
287
+
288
+
289
+ def first_error_line(result: ValidationResult, schema_path: str) -> str:
290
+ """The design-doc 'Invalid schema (structural)' one-liner for the first error."""
291
+ e = result.errors[0]
292
+ return (
293
+ f"Schema invalid: {schema_path} — {e.path}: {e.msg} "
294
+ "Run `cu analyzer validate --help` for validation guidance."
295
+ )
296
+
297
+
298
+ def parse_and_validate(text: str, *, api_version: Optional[str] = None
299
+ ) -> Tuple[ValidationResult, Optional[dict]]:
300
+ """Parse JSON text, then validate. JSON errors are returned as a validation
301
+ error (exit-2 territory) rather than raised."""
302
+ try:
303
+ body = json.loads(text)
304
+ except json.JSONDecodeError as exc:
305
+ res = ValidationResult(
306
+ ok=False,
307
+ errors=[Finding("$", f"file is not valid JSON: {exc.msg} "
308
+ f"at line {exc.lineno} col {exc.colno}.")],
309
+ )
310
+ return res, None
311
+ result = validate_schema(body, api_version=api_version)
312
+ return result, body if isinstance(body, dict) else None
@@ -0,0 +1,75 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Convert CU and Azure SDK values into frontend-neutral plain values."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping, Sequence
9
+ from dataclasses import asdict, is_dataclass
10
+ from datetime import date, datetime, time
11
+ from enum import Enum
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .errors import ValidationError
16
+
17
+
18
+ def to_plain_value(value: Any) -> Any:
19
+ """Recursively convert supported values to dictionaries, lists, and scalars."""
20
+
21
+ return _to_plain_value(value, seen=set())
22
+
23
+
24
+ def _to_plain_value(value: Any, *, seen: set[int]) -> Any:
25
+ if value is None or isinstance(value, (str, int, float, bool)):
26
+ return value
27
+ if isinstance(value, Enum):
28
+ return _to_plain_value(value.value, seen=seen)
29
+ if isinstance(value, (date, datetime, time)):
30
+ return value.isoformat()
31
+ if isinstance(value, Path):
32
+ return str(value)
33
+
34
+ identity = id(value)
35
+ if identity in seen:
36
+ raise ValidationError("cannot serialize a cyclic result value")
37
+
38
+ if hasattr(value, "as_dict"):
39
+ seen.add(identity)
40
+ try:
41
+ converted = value.as_dict()
42
+ except Exception as exc:
43
+ raise ValidationError(
44
+ f"failed to serialize {type(value).__name__} through as_dict()"
45
+ ) from exc
46
+ finally:
47
+ seen.remove(identity)
48
+ return _to_plain_value(converted, seen=seen)
49
+
50
+ if is_dataclass(value) and not isinstance(value, type):
51
+ seen.add(identity)
52
+ try:
53
+ converted = asdict(value)
54
+ finally:
55
+ seen.remove(identity)
56
+ return _to_plain_value(converted, seen=seen)
57
+
58
+ if isinstance(value, Mapping):
59
+ seen.add(identity)
60
+ try:
61
+ return {
62
+ str(key): _to_plain_value(item, seen=seen)
63
+ for key, item in value.items()
64
+ }
65
+ finally:
66
+ seen.remove(identity)
67
+
68
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
69
+ seen.add(identity)
70
+ try:
71
+ return [_to_plain_value(item, seen=seen) for item in value]
72
+ finally:
73
+ seen.remove(identity)
74
+
75
+ raise ValidationError(f"unsupported result value: {type(value).__name__}")
@@ -0,0 +1,71 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Static service-option metadata shared by command frontends."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from .command_spec import ArgumentValueType, SurfaceClassification
11
+
12
+ DEFAULT_API_VERSION = "2025-11-01"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ServiceOptionSpec:
17
+ key: str
18
+ name: str
19
+ parser_name: str
20
+ help: str
21
+ value_type: ArgumentValueType = ArgumentValueType.STRING
22
+ aliases: tuple[str, ...] = ()
23
+ required: bool = False
24
+ default: object = None
25
+ choices: tuple[str, ...] = ()
26
+ sensitive: bool = False
27
+ classification: SurfaceClassification = SurfaceClassification.COMMON
28
+
29
+
30
+ ENDPOINT = ServiceOptionSpec(
31
+ key="endpoint",
32
+ name="--endpoint",
33
+ parser_name="endpoint",
34
+ help="Microsoft Foundry resource endpoint for Content Understanding.",
35
+ )
36
+ API_VERSION = ServiceOptionSpec(
37
+ key="api-version",
38
+ name="--api-version",
39
+ parser_name="api_version",
40
+ help="Content Understanding service API version.",
41
+ default=DEFAULT_API_VERSION,
42
+ )
43
+ AUTH_MODE = ServiceOptionSpec(
44
+ key="auth-mode",
45
+ name="--auth-mode",
46
+ parser_name="auth_mode",
47
+ help="Authentication mode.",
48
+ default="login",
49
+ choices=("login", "key"),
50
+ )
51
+ API_KEY = ServiceOptionSpec(
52
+ key="api-key",
53
+ name="--api-key",
54
+ parser_name="api_key",
55
+ help="Microsoft Foundry resource API key.",
56
+ sensitive=True,
57
+ )
58
+
59
+ SERVICE_OPTIONS = (ENDPOINT, API_VERSION, AUTH_MODE, API_KEY)
60
+ _SERVICE_OPTIONS_BY_KEY = {option.key: option for option in SERVICE_OPTIONS}
61
+
62
+ if len(_SERVICE_OPTIONS_BY_KEY) != len(SERVICE_OPTIONS):
63
+ raise RuntimeError("duplicate service option key")
64
+
65
+
66
+ def get_service_option(key: str) -> ServiceOptionSpec:
67
+ return _SERVICE_OPTIONS_BY_KEY[key]
68
+
69
+
70
+ def service_options_for(keys: tuple[str, ...]) -> tuple[ServiceOptionSpec, ...]:
71
+ return tuple(get_service_option(key) for key in keys)
@@ -0,0 +1,141 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Spec-backed structural validation against the bundled CU OpenAPI (Swagger 2.0).
5
+
6
+ The CU service contract is published as ``ContentUnderstanding.json`` (Swagger
7
+ 2.0, whose ``definitions`` are JSON Schema Draft 4). We bundle that JSON per
8
+ api-version and validate an analyzer body against the ``ContentAnalyzer``
9
+ definition with ``jsonschema`` — so the structural rules come straight from the
10
+ service contract instead of a hand-maintained list.
11
+
12
+ Two adaptations are applied:
13
+
14
+ * **Create-view** — the ``ContentAnalyzer`` model marks server-generated fields
15
+ (``status``, ``createdAt``, ``lastModifiedAt``, ``analyzerId``) as ``readOnly``
16
+ yet lists them in ``required``. Those don't exist in an authoring/create body,
17
+ so we drop ``readOnly`` properties from every object's ``required`` list.
18
+ * **Graceful degradation** — if the bundled spec or ``jsonschema`` is
19
+ unavailable, ``validate_against_spec`` returns an OK result with no findings so
20
+ the primary (rule-based) validator is never blocked.
21
+
22
+ This module is an *additional* structural gate (opt-in via ``cu analyzer
23
+ validate --spec``); the curated, message-friendly checks in ``schema_validate``
24
+ remain the default.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import copy
30
+ import json
31
+ from functools import lru_cache
32
+ from importlib import resources as ir
33
+ from typing import Any, List
34
+
35
+ from .schema_validation import Finding, ValidationResult
36
+ from .service_options import DEFAULT_API_VERSION
37
+
38
+ _ANALYZER_DEF = "ContentAnalyzer"
39
+ _SPEC_URI = "urn:cu-spec"
40
+
41
+
42
+ def _spec_resource(api_version: str) -> str:
43
+ return f"openapi/{api_version}/ContentUnderstanding.json"
44
+
45
+
46
+ @lru_cache(maxsize=None)
47
+ def _load_spec(api_version: str) -> dict | None:
48
+ try:
49
+ text = (ir.files("cu_cli_core")
50
+ .joinpath(f"resources/{_spec_resource(api_version)}")
51
+ .read_text(encoding="utf-8"))
52
+ data = json.loads(text)
53
+ return data if isinstance(data, dict) else None
54
+ except Exception:
55
+ return None
56
+
57
+
58
+ def spec_available(api_version: str = DEFAULT_API_VERSION) -> bool:
59
+ """True when the bundled OpenAPI spec exists and has the analyzer definition."""
60
+ spec = _load_spec(api_version)
61
+ return bool(spec and isinstance(spec.get("definitions"), dict)
62
+ and _ANALYZER_DEF in spec["definitions"])
63
+
64
+
65
+ def spec_allowed_types(api_version: str = DEFAULT_API_VERSION) -> List[str]:
66
+ spec = _load_spec(api_version) or {}
67
+ enum = (spec.get("definitions", {}).get("ContentFieldType", {}) or {}).get("enum") or []
68
+ return sorted(str(v) for v in enum)
69
+
70
+
71
+ def spec_allowed_methods(api_version: str = DEFAULT_API_VERSION) -> List[str]:
72
+ spec = _load_spec(api_version) or {}
73
+ enum = (spec.get("definitions", {}).get("GenerationMethod", {}) or {}).get("enum") or []
74
+ return sorted(str(v) for v in enum)
75
+
76
+
77
+ def _create_view_defs(defs: dict) -> dict:
78
+ """Return a copy of *defs* with ``readOnly`` props removed from ``required``."""
79
+ out = copy.deepcopy(defs)
80
+ for d in out.values():
81
+ if isinstance(d, dict) and d.get("type") == "object" and isinstance(d.get("required"), list):
82
+ props = d.get("properties") or {}
83
+ d["required"] = [
84
+ r for r in d["required"]
85
+ if not (isinstance(props.get(r), dict) and props[r].get("readOnly"))
86
+ ]
87
+ return out
88
+
89
+
90
+ @lru_cache(maxsize=None)
91
+ def _build_validator(api_version: str):
92
+ spec = _load_spec(api_version)
93
+ if not spec:
94
+ return None
95
+ defs = spec.get("definitions")
96
+ if not isinstance(defs, dict) or _ANALYZER_DEF not in defs:
97
+ return None
98
+ try:
99
+ from jsonschema import Draft4Validator
100
+ from referencing import Registry, Resource
101
+ from referencing.jsonschema import DRAFT4
102
+ except Exception: # pragma: no cover - deps are declared, defensive only
103
+ return None
104
+
105
+ create_view = {"definitions": _create_view_defs(defs)}
106
+ resource = Resource.from_contents(create_view, default_specification=DRAFT4)
107
+ registry = Registry().with_resource(uri=_SPEC_URI, resource=resource)
108
+ return Draft4Validator(
109
+ {"$ref": f"{_SPEC_URI}#/definitions/{_ANALYZER_DEF}"},
110
+ registry=registry,
111
+ )
112
+
113
+
114
+ def _dotted(parts: list) -> str:
115
+ out = ""
116
+ for p in parts:
117
+ if isinstance(p, int):
118
+ out += f"[{p}]"
119
+ else:
120
+ out += f".{p}" if out else str(p)
121
+ return out or "$"
122
+
123
+
124
+ def validate_against_spec(body: Any, *, api_version: str = DEFAULT_API_VERSION) -> ValidationResult:
125
+ """Validate *body* against the bundled CU ``ContentAnalyzer`` (create-view).
126
+
127
+ Returns an OK result with no findings when the spec or ``jsonschema`` are
128
+ unavailable, so it never blocks the primary validator.
129
+ """
130
+ validator = _build_validator(api_version)
131
+ if validator is None:
132
+ return ValidationResult(ok=True)
133
+ if not isinstance(body, dict):
134
+ return ValidationResult(
135
+ ok=False, errors=[Finding("$", "schema root must be a JSON object.")]
136
+ )
137
+ errors: List[Finding] = []
138
+ for err in sorted(validator.iter_errors(body), key=lambda e: list(e.absolute_path)):
139
+ path = _dotted(list(err.absolute_path))
140
+ errors.append(Finding(path, f"{err.message} (per CU {api_version} spec)"))
141
+ return ValidationResult(ok=not errors, errors=errors)
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: cu-cli-core
3
+ Version: 0.1.0b1
4
+ Summary: Framework-neutral command contracts and operations for Azure Content Understanding CLIs.
5
+ Author: Microsoft Corporation
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: azure-ai-contentunderstanding>=1.2.0b3
11
+ Requires-Dist: jsonschema>=4.18
12
+ Requires-Dist: referencing>=0.30
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.4; extra == "dev"
15
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
16
+ Requires-Dist: mypy<2,>=1.8; extra == "dev"
17
+ Requires-Dist: ruff<0.16,>=0.5; extra == "dev"
18
+
19
+ # CU CLI Core
20
+
21
+ `cu-cli-core` is an implementation dependency shared by official Azure Content
22
+ Understanding command-line frontends. Install
23
+ [`cu-cli`](https://pypi.org/project/cu-cli/) instead.
24
+
25
+ Direct use of this package is unsupported. Its contracts may change between
26
+ compatible CU CLI releases.
@@ -0,0 +1,29 @@
1
+ cu_cli_core/__init__.py,sha256=DHWkIHgv3xPo9hd_P15Q0CjynCcSCWi57qlqGCgLy1k,290
2
+ cu_cli_core/analysis.py,sha256=pePCCKLsVZnzTc6tnZqky7BFB_-XU6WYH63evcmBxV0,12500
3
+ cu_cli_core/client.py,sha256=8x1RT_taZeCRdo2mWNjaMWXMZF0K6MitZ2ldEKatDOQ,1177
4
+ cu_cli_core/command_spec.py,sha256=0i3OSa6hwJt325hj4cuSO6xmnnu9vipNFMyV7GfniIs,36043
5
+ cu_cli_core/contracts.py,sha256=OZCQsA4e6hrj747nbKT8-NRx_1diFNQ3f27Vlp87nx4,11016
6
+ cu_cli_core/defaults.py,sha256=EBzIocHnK8Q3L3rYJanBrug0qTD9aMAiORo6izKftD0,4854
7
+ cu_cli_core/environment.py,sha256=ThvebulIgf_kKSyKncxQ_Mw1z4_8PGHYwLIfDmu08XY,3258
8
+ cu_cli_core/errors.py,sha256=wDCZZghsoUarc47k2drvbTaxYQAUUdz7sc_WdxC1GMQ,1728
9
+ cu_cli_core/input_planning.py,sha256=xrlHZ_8H25VEOu0waMICcCOgueYI4EZ9_WpGPp9Q3eQ,11147
10
+ cu_cli_core/profiles.py,sha256=4z6H1hy2QLwZIcp6e5rV71-bX4ur3H4rmBRtxjouYSU,22800
11
+ cu_cli_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ cu_cli_core/schema.py,sha256=uJj4PlNi920m6jWoj0VF3vC6vsr8dbhpoJbEYkko2Y8,14137
13
+ cu_cli_core/schema_validation.py,sha256=GR2z4SG7CDbNMbOgSaI72A8BVG9KYjh7mQIrpGDdfY8,13442
14
+ cu_cli_core/serialization.py,sha256=NPNlZQesjsPuJB2nbfalcRrQQ72sSICkK4W_Rbki0B4,2330
15
+ cu_cli_core/service_options.py,sha256=hXQJ8kfQlgWROxY3RGkHOowd292KYxu9OWF9xnJBoy8,1944
16
+ cu_cli_core/spec_validation.py,sha256=De-YAVxfqeEmzR_HKLqVy01vdNLfq7U4RCgdMFDppbM,5385
17
+ cu_cli_core/operations/__init__.py,sha256=a9Po_SPpPzPCFtAF8RNG5FwsFz7gDYir6-vmTydCMww,130
18
+ cu_cli_core/operations/analysis.py,sha256=yCaWq7vimXI-V2ClXnC3mf4445LnZNu-6TMpFsOzHck,9376
19
+ cu_cli_core/operations/analyzer_copy.py,sha256=uvTgVb3Lf5TpvhoPXvVzQj_8rry1EF3JHKgqJJhMk0Q,13751
20
+ cu_cli_core/operations/analyzers.py,sha256=HGICe9b91kFVRRVYltObRRr5M_4mtmAGisvLVhm1OCI,2715
21
+ cu_cli_core/operations/profiles.py,sha256=VPZppZ0blbFBKamZvBnhwbtfgO8rW3NXEQJokLDD-So,3708
22
+ cu_cli_core/operations/schema.py,sha256=3ILQTqlzvS14wzWGhBELkRPeUKQNZ5FkHKiGiPYUu0M,1315
23
+ cu_cli_core/operations/validation.py,sha256=WbfSvO9hPdc5uZnyYzH0jNZSub0eFHTwtjUQthrAQv8,1383
24
+ cu_cli_core/resources/openapi/2025-11-01/ContentUnderstanding.json,sha256=_lEnj8-0aEn7mSQUPtZsHHffSKFlciZUgNKw4__CFhQ,111923
25
+ cu_cli_core/resources/openapi/2026-06-01-preview/ContentUnderstanding.json,sha256=xb8oVxyD0q_j7bmvr1LUEwXUWO7aaR3mL73PUPucXhQ,127937
26
+ cu_cli_core-0.1.0b1.dist-info/METADATA,sha256=lpiAjBT_QbuOvEhSqGuzr5OpDzVjGg1HHwoXcYDniwA,925
27
+ cu_cli_core-0.1.0b1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
28
+ cu_cli_core-0.1.0b1.dist-info/top_level.txt,sha256=bvblrW7CaZDgJVrnNCtKh5AACehffTYE3-xTNIEIpFw,12
29
+ cu_cli_core-0.1.0b1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cu_cli_core