pal-found-cli 0.1.2__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 (89) hide show
  1. pal_found_cli/__init__.py +1 -0
  2. pal_found_cli/_version.py +24 -0
  3. pal_found_cli/admin/__init__.py +2 -0
  4. pal_found_cli/admin/scripts/__init__.py +2 -0
  5. pal_found_cli/admin/scripts/pal_found_admin_cli.py +600 -0
  6. pal_found_cli/aip_agents/__init__.py +1 -0
  7. pal_found_cli/aip_agents/metadata-allow-list.md +20 -0
  8. pal_found_cli/aip_agents/scripts/__init__.py +1 -0
  9. pal_found_cli/aip_agents/scripts/pal_found_aip_agents_cli.py +704 -0
  10. pal_found_cli/audit/__init__.py +1 -0
  11. pal_found_cli/audit/metadata-allow-list.md +4 -0
  12. pal_found_cli/audit/scripts/__init__.py +1 -0
  13. pal_found_cli/audit/scripts/pal_found_audit_cli.py +387 -0
  14. pal_found_cli/checkpoints/__init__.py +9 -0
  15. pal_found_cli/checkpoints/metadata-allow-list.md +7 -0
  16. pal_found_cli/checkpoints/scripts/__init__.py +1 -0
  17. pal_found_cli/checkpoints/scripts/pal_found_checkpoints_cli.py +527 -0
  18. pal_found_cli/common/__init__.py +56 -0
  19. pal_found_cli/common/access_control_guard.py +591 -0
  20. pal_found_cli/common/async_client_factory.py +161 -0
  21. pal_found_cli/common/auth_provider.py +82 -0
  22. pal_found_cli/common/binary_download_handler.py +288 -0
  23. pal_found_cli/common/config_loader.py +388 -0
  24. pal_found_cli/common/error_serializer.py +412 -0
  25. pal_found_cli/common/log_setup.py +241 -0
  26. pal_found_cli/common/metadata-allow-list.md +169 -0
  27. pal_found_cli/common/output_formatter.py +267 -0
  28. pal_found_cli/common/pagination_helper.py +212 -0
  29. pal_found_cli/common/retry.py +617 -0
  30. pal_found_cli/common/sdk_error_utils.py +112 -0
  31. pal_found_cli/common/session_manager.py +461 -0
  32. pal_found_cli/common/tracing_provider.py +107 -0
  33. pal_found_cli/connectivity/__init__.py +9 -0
  34. pal_found_cli/connectivity/metadata-allow-list.md +24 -0
  35. pal_found_cli/connectivity/scripts/__init__.py +1 -0
  36. pal_found_cli/connectivity/scripts/pal_found_connectivity_cli.py +720 -0
  37. pal_found_cli/data_health/__init__.py +9 -0
  38. pal_found_cli/data_health/metadata-allow-list.md +10 -0
  39. pal_found_cli/data_health/scripts/__init__.py +1 -0
  40. pal_found_cli/data_health/scripts/pal_found_data_health_cli.py +458 -0
  41. pal_found_cli/datasets/__init__.py +1 -0
  42. pal_found_cli/datasets/scripts/__init__.py +1 -0
  43. pal_found_cli/datasets/scripts/pal_found_datasets_cli.py +732 -0
  44. pal_found_cli/filesystem/__init__.py +2 -0
  45. pal_found_cli/filesystem/scripts/__init__.py +2 -0
  46. pal_found_cli/filesystem/scripts/pal_found_filesystem_cli.py +659 -0
  47. pal_found_cli/functions/__init__.py +2 -0
  48. pal_found_cli/functions/scripts/__init__.py +2 -0
  49. pal_found_cli/functions/scripts/pal_found_functions_cli.py +389 -0
  50. pal_found_cli/language_models/__init__.py +1 -0
  51. pal_found_cli/language_models/metadata-allow-list.md +6 -0
  52. pal_found_cli/language_models/scripts/__init__.py +1 -0
  53. pal_found_cli/language_models/scripts/pal_found_language_models_cli.py +342 -0
  54. pal_found_cli/media_sets/__init__.py +9 -0
  55. pal_found_cli/media_sets/metadata-allow-list.md +23 -0
  56. pal_found_cli/media_sets/scripts/__init__.py +1 -0
  57. pal_found_cli/media_sets/scripts/pal_found_media_sets_cli.py +642 -0
  58. pal_found_cli/models/__init__.py +5 -0
  59. pal_found_cli/models/metadata-allow-list.md +27 -0
  60. pal_found_cli/models/scripts/__init__.py +1 -0
  61. pal_found_cli/models/scripts/pal_found_models_cli.py +714 -0
  62. pal_found_cli/ontologies/__init__.py +1 -0
  63. pal_found_cli/ontologies/scripts/__init__.py +1 -0
  64. pal_found_cli/ontologies/scripts/pal_found_ontologies_cli.py +1225 -0
  65. pal_found_cli/orchestration/__init__.py +9 -0
  66. pal_found_cli/orchestration/metadata-allow-list.md +24 -0
  67. pal_found_cli/orchestration/scripts/__init__.py +1 -0
  68. pal_found_cli/orchestration/scripts/pal_found_orchestration_cli.py +640 -0
  69. pal_found_cli/sql_queries/__init__.py +9 -0
  70. pal_found_cli/sql_queries/metadata-allow-list.md +9 -0
  71. pal_found_cli/sql_queries/scripts/__init__.py +1 -0
  72. pal_found_cli/sql_queries/scripts/pal_found_sql_queries_cli.py +514 -0
  73. pal_found_cli/streams/__init__.py +9 -0
  74. pal_found_cli/streams/metadata-allow-list.md +19 -0
  75. pal_found_cli/streams/scripts/__init__.py +1 -0
  76. pal_found_cli/streams/scripts/pal_found_streams_cli.py +615 -0
  77. pal_found_cli/third_party_applications/__init__.py +9 -0
  78. pal_found_cli/third_party_applications/metadata-allow-list.md +13 -0
  79. pal_found_cli/third_party_applications/scripts/__init__.py +1 -0
  80. pal_found_cli/third_party_applications/scripts/pal_found_third_party_applications_cli.py +547 -0
  81. pal_found_cli/widgets/__init__.py +9 -0
  82. pal_found_cli/widgets/metadata-allow-list.md +12 -0
  83. pal_found_cli/widgets/scripts/__init__.py +1 -0
  84. pal_found_cli/widgets/scripts/pal_found_widgets_cli.py +571 -0
  85. pal_found_cli-0.1.2.dist-info/METADATA +74 -0
  86. pal_found_cli-0.1.2.dist-info/RECORD +89 -0
  87. pal_found_cli-0.1.2.dist-info/WHEEL +5 -0
  88. pal_found_cli-0.1.2.dist-info/entry_points.txt +19 -0
  89. pal_found_cli-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,642 @@
1
+ #!/usr/bin/env python3
2
+ """Foundry Media Sets CLI - 19 canonical Media Sets API v2 operations.
3
+
4
+ Exposes exactly 19 public ``foundry_sdk.v2.media_sets`` operations as CLI
5
+ subcommands through the single ``MediaSet`` client path:
6
+
7
+ - media-set (19): abort, calculate, clear, commit, create, get, get-result,
8
+ get-rid-by-path, get-status, info, metadata, read, read-original,
9
+ reference, register, retrieve, transform, upload, upload-media
10
+
11
+ Four operations return raw ``bytes`` (``get_result``, ``read``,
12
+ ``read_original``, ``retrieve``). These are dispatched through
13
+ ``with_streaming_response`` and persisted via ``BinaryDownloadHandler``,
14
+ bounded by the FR-DL size limit with the JSON envelope emitted to stdout.
15
+
16
+ Usage: pal-found-media-sets <resource> <operation> [options]
17
+ Output: JSON/TOON on stdout, NDJSON diagnostics on stderr (ADR-004, ADR-005).
18
+ Exit codes per ADR-001 taxonomy.
19
+ Access control: AccessControlGuard before client construction and file
20
+ effects (SRS 4.2, ADR-007). Retry: transient conditions only per ADR-002.
21
+ Tracing: SDK-native B3 via invocation_scope; include_attribution=True
22
+ per FR-ATTR-4 (media_sets.media_set.* is explicitly attributed).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import asyncio
29
+ import json
30
+ import logging
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Any, NoReturn
34
+
35
+ from pal_found_cli.common.access_control_guard import (
36
+ AccessControlError,
37
+ AccessControlGuard,
38
+ )
39
+ from pal_found_cli.common.async_client_factory import AsyncClientFactory
40
+ from pal_found_cli.common.binary_download_handler import BinaryDownloadHandler
41
+ from pal_found_cli.common.config_loader import ConfigLoader, ConfigurationError
42
+ from pal_found_cli.common.error_serializer import (
43
+ EXIT_ACCESS_CONTROL,
44
+ EXIT_AUTH,
45
+ EXIT_CONFIGURATION,
46
+ EXIT_NOT_FOUND,
47
+ EXIT_PERMISSION_DENIED,
48
+ EXIT_RATE_LIMIT,
49
+ EXIT_SERVER_ERROR,
50
+ EXIT_SUCCESS,
51
+ EXIT_TIMEOUT,
52
+ EXIT_USER_INPUT,
53
+ ErrorSerializer,
54
+ )
55
+ from pal_found_cli.common.log_setup import LogSetup
56
+ from pal_found_cli.common.output_formatter import OutputFormatter
57
+ from pal_found_cli.common.retry import RetryHandler
58
+ from pal_found_cli.common.sdk_error_utils import sdk_exception_exit_code, sdk_http_status
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+ OperationSpec = dict[str, Any]
63
+
64
+ _METADATA_ALLOWLIST_PATH = Path(__file__).resolve().parents[1] / "metadata-allow-list.md"
65
+
66
+ # The four file-producing streamed download commands (DESIGN-018). Each
67
+ # acquires a streaming SDK response and persists bounded content through
68
+ # BinaryDownloadHandler before opening the destination.
69
+ DOWNLOAD_OPS: frozenset[tuple[str, str]] = frozenset(
70
+ {
71
+ ("media_set", "get_result"),
72
+ ("media_set", "read"),
73
+ ("media_set", "read_original"),
74
+ ("media_set", "retrieve"),
75
+ }
76
+ )
77
+
78
+ # Binary upload operations that read a local file into the bytes body.
79
+ _UPLOAD_OPS: frozenset[tuple[str, str]] = frozenset(
80
+ {
81
+ ("media_set", "upload"),
82
+ ("media_set", "upload_media"),
83
+ }
84
+ )
85
+
86
+ # Bounded binary upload size cap (aligned with FR-DL limits).
87
+ _UPLOAD_MAX_BYTES = 16 * 1024 * 1024
88
+
89
+
90
+ class CLIInputError(ValueError):
91
+ """A locally generated input error whose message never echoes values."""
92
+
93
+ exit_code = EXIT_USER_INPUT
94
+
95
+
96
+ class _ArgumentParser(argparse.ArgumentParser):
97
+ """Raise parser failures into the structured error boundary."""
98
+
99
+ def error(self, message: str) -> NoReturn:
100
+ raise CLIInputError("Invalid command input")
101
+
102
+
103
+ def _op(
104
+ resource: str,
105
+ operation: str,
106
+ client_path: tuple[str, ...],
107
+ method: str,
108
+ positional: tuple[str, ...] = (),
109
+ required: tuple[str, ...] = (),
110
+ optional: tuple[str, ...] = (),
111
+ *,
112
+ json_args: frozenset[str] = frozenset(),
113
+ list_json_args: frozenset[str] = frozenset(),
114
+ ) -> OperationSpec:
115
+ """Build one operation specification entry."""
116
+ return {
117
+ "resource": resource,
118
+ "operation": operation,
119
+ "client_path": client_path,
120
+ "method": method,
121
+ "positional": positional,
122
+ "required": required,
123
+ "optional": optional,
124
+ "json_args": json_args,
125
+ "list_json_args": list_json_args,
126
+ }
127
+
128
+
129
+ OP_SPECS: tuple[OperationSpec, ...] = (
130
+ _op(
131
+ "media_set",
132
+ "abort",
133
+ ("MediaSet",),
134
+ "abort",
135
+ ("media_set_rid", "transaction_id"),
136
+ optional=("preview",),
137
+ ),
138
+ _op(
139
+ "media_set",
140
+ "calculate",
141
+ ("MediaSet",),
142
+ "calculate",
143
+ ("media_set_rid", "media_item_rid"),
144
+ optional=("preview", "read_token"),
145
+ ),
146
+ _op(
147
+ "media_set",
148
+ "clear",
149
+ ("MediaSet",),
150
+ "clear",
151
+ ("media_set_rid",),
152
+ required=("media_item_path",),
153
+ optional=("branch_name", "branch_rid", "preview", "transaction_id", "view_rid"),
154
+ ),
155
+ _op(
156
+ "media_set",
157
+ "commit",
158
+ ("MediaSet",),
159
+ "commit",
160
+ ("media_set_rid", "transaction_id"),
161
+ optional=("preview",),
162
+ ),
163
+ _op(
164
+ "media_set",
165
+ "create",
166
+ ("MediaSet",),
167
+ "create",
168
+ ("media_set_rid",),
169
+ optional=("branch_name", "preview"),
170
+ ),
171
+ _op(
172
+ "media_set",
173
+ "get",
174
+ ("MediaSet",),
175
+ "get",
176
+ ("media_set_rid",),
177
+ optional=("preview",),
178
+ ),
179
+ _op(
180
+ "media_set",
181
+ "get_result",
182
+ ("MediaSet",),
183
+ "get_result",
184
+ ("media_set_rid", "media_item_rid", "transformation_job_id"),
185
+ optional=("output", "preview", "token"),
186
+ ),
187
+ _op(
188
+ "media_set",
189
+ "get_rid_by_path",
190
+ ("MediaSet",),
191
+ "get_rid_by_path",
192
+ ("media_set_rid",),
193
+ required=("media_item_path",),
194
+ optional=("branch_name", "branch_rid", "preview", "view_rid"),
195
+ ),
196
+ _op(
197
+ "media_set",
198
+ "get_status",
199
+ ("MediaSet",),
200
+ "get_status",
201
+ ("media_set_rid", "media_item_rid", "transformation_job_id"),
202
+ optional=("preview", "token"),
203
+ ),
204
+ _op(
205
+ "media_set",
206
+ "info",
207
+ ("MediaSet",),
208
+ "info",
209
+ ("media_set_rid", "media_item_rid"),
210
+ optional=("preview", "read_token"),
211
+ ),
212
+ _op(
213
+ "media_set",
214
+ "metadata",
215
+ ("MediaSet",),
216
+ "metadata",
217
+ ("media_set_rid", "media_item_rid"),
218
+ optional=("preview", "read_token"),
219
+ ),
220
+ _op(
221
+ "media_set",
222
+ "read",
223
+ ("MediaSet",),
224
+ "read",
225
+ ("media_set_rid", "media_item_rid"),
226
+ optional=("output", "preview", "read_token"),
227
+ ),
228
+ _op(
229
+ "media_set",
230
+ "read_original",
231
+ ("MediaSet",),
232
+ "read_original",
233
+ ("media_set_rid", "media_item_rid"),
234
+ optional=("output", "preview", "read_token"),
235
+ ),
236
+ _op(
237
+ "media_set",
238
+ "reference",
239
+ ("MediaSet",),
240
+ "reference",
241
+ ("media_set_rid", "media_item_rid"),
242
+ optional=("preview", "read_token"),
243
+ ),
244
+ _op(
245
+ "media_set",
246
+ "register",
247
+ ("MediaSet",),
248
+ "register",
249
+ ("media_set_rid",),
250
+ required=("physical_item_name",),
251
+ optional=("branch_name", "media_item_path", "preview", "transaction_id", "view_rid"),
252
+ ),
253
+ _op(
254
+ "media_set",
255
+ "retrieve",
256
+ ("MediaSet",),
257
+ "retrieve",
258
+ ("media_set_rid", "media_item_rid"),
259
+ optional=("output", "preview", "read_token"),
260
+ ),
261
+ _op(
262
+ "media_set",
263
+ "transform",
264
+ ("MediaSet",),
265
+ "transform",
266
+ ("media_set_rid", "media_item_rid"),
267
+ required=("transformation",),
268
+ optional=("preview", "token"),
269
+ json_args=frozenset({"transformation"}),
270
+ ),
271
+ _op(
272
+ "media_set",
273
+ "upload",
274
+ ("MediaSet",),
275
+ "upload",
276
+ ("media_set_rid",),
277
+ required=("file",),
278
+ optional=("branch_name", "branch_rid", "media_item_path", "media_item_rid", "preview", "transaction_id", "view_rid"),
279
+ ),
280
+ _op(
281
+ "media_set",
282
+ "upload_media",
283
+ ("MediaSet",),
284
+ "upload_media",
285
+ required=("file", "filename"),
286
+ optional=("media_item_rid", "preview"),
287
+ ),
288
+ )
289
+
290
+ OPERATION_BY_RESOURCE: dict[str, dict[str, OperationSpec]] = {}
291
+ for _spec in OP_SPECS:
292
+ OPERATION_BY_RESOURCE.setdefault(_spec["resource"], {})[_spec["operation"]] = _spec
293
+
294
+ # Structured JSON arguments across the catalog use the ``-json`` flag suffix.
295
+ _JSON_ARG_NAMES: frozenset[str] = frozenset(
296
+ name
297
+ for spec in OP_SPECS
298
+ for name in (*spec["json_args"], *spec["list_json_args"])
299
+ )
300
+
301
+ # Boolean-typed SDK options registered as store_true flags.
302
+ _BOOLEAN_ARG_NAMES: frozenset[str] = frozenset({"preview"})
303
+
304
+
305
+ def _common_parser() -> _ArgumentParser:
306
+ """Build operation-level common options."""
307
+ parser = _ArgumentParser(add_help=False)
308
+ parser.add_argument("--timeout", type=int, default=None)
309
+ parser.add_argument("--format", choices=("json", "toon", "auto"), default="auto")
310
+ parser.add_argument("--pretty", action="store_true")
311
+ return parser
312
+
313
+
314
+ def _kebab(name: str) -> str:
315
+ """Convert snake_case names to CLI kebab-case."""
316
+ return name.replace("_", "-")
317
+
318
+
319
+ def _add_kwarg(
320
+ parser: argparse.ArgumentParser, arg_name: str, *, required: bool
321
+ ) -> None:
322
+ """Add one named operation argument."""
323
+ if arg_name in _JSON_ARG_NAMES:
324
+ flag = "--" + _kebab(arg_name) + "-json"
325
+ elif arg_name in _BOOLEAN_ARG_NAMES:
326
+ flag = "--" + _kebab(arg_name)
327
+ parser.add_argument(flag, action="store_true", default=None, dest=arg_name)
328
+ return
329
+ else:
330
+ flag = "--" + _kebab(arg_name)
331
+ parser.add_argument(flag, required=required, default=None, dest=arg_name)
332
+
333
+
334
+ def build_parser() -> argparse.ArgumentParser:
335
+ """Build argparse parser with all 19 Media Sets operations."""
336
+ parser = _ArgumentParser(
337
+ prog="pal-found-media-sets",
338
+ description="Foundry Media Sets CLI - 19 Media Sets API v2 operations",
339
+ )
340
+ subparsers = parser.add_subparsers(dest="resource", help="Resource type")
341
+
342
+ for resource in sorted(OPERATION_BY_RESOURCE):
343
+ res_parser = subparsers.add_parser(_kebab(resource))
344
+ op_sub = res_parser.add_subparsers(dest="operation")
345
+ for operation, spec in sorted(OPERATION_BY_RESOURCE[resource].items()):
346
+ op_parser = op_sub.add_parser(
347
+ _kebab(operation), parents=[_common_parser()]
348
+ )
349
+ for arg_name in spec["positional"]:
350
+ op_parser.add_argument(arg_name)
351
+ for arg_name in spec["required"]:
352
+ _add_kwarg(op_parser, arg_name, required=True)
353
+ for arg_name in spec["optional"]:
354
+ _add_kwarg(op_parser, arg_name, required=False)
355
+
356
+ return parser
357
+
358
+
359
+ def _spec_for(resource: str, operation: str) -> OperationSpec:
360
+ """Return one catalog operation specification."""
361
+ try:
362
+ return OPERATION_BY_RESOURCE[resource][operation]
363
+ except KeyError as exc:
364
+ raise CLIInputError(f"Unknown operation: {resource}.{operation}") from exc
365
+
366
+
367
+ def _get_client(root_client: Any, client_path: tuple[str, ...]) -> Any:
368
+ """Resolve the exact public nested Media Sets SDK resource."""
369
+ client = root_client.media_sets
370
+ for attribute in client_path:
371
+ client = getattr(client, attribute)
372
+ return client
373
+
374
+
375
+ def _required_text(value: Any, *, field: str) -> str:
376
+ """Validate a non-empty string without echoing its value."""
377
+ if not isinstance(value, str) or not value.strip():
378
+ raise CLIInputError(f"{field} must not be empty")
379
+ return value
380
+
381
+
382
+ def _decode_json(value: str, *, field: str) -> Any:
383
+ try:
384
+ return json.loads(value)
385
+ except json.JSONDecodeError as exc:
386
+ raise CLIInputError(f"{field} must contain valid JSON") from exc
387
+
388
+
389
+ def _parse_json_object(value: str, *, field: str) -> dict[str, Any]:
390
+ """Decode one JSON object."""
391
+ result = _decode_json(value, field=field)
392
+ if not isinstance(result, dict):
393
+ raise CLIInputError(f"{field} must be a JSON object")
394
+ return result
395
+
396
+
397
+ def _validate_timeout(value: int) -> int:
398
+ """Validate the effective per-attempt timeout from ADR-002."""
399
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 3600:
400
+ raise CLIInputError("timeout must be between 1 and 3600 seconds")
401
+ return value
402
+
403
+
404
+ def _read_file_bounded(path_value: str, *, field: str) -> bytes:
405
+ """Read a local file for binary upload with a hard size bound."""
406
+ path = Path(path_value)
407
+ if not path.is_file():
408
+ raise CLIInputError(f"{field} must reference an existing file")
409
+ size = path.stat().st_size
410
+ if size > _UPLOAD_MAX_BYTES:
411
+ raise CLIInputError(f"{field} exceeds the maximum upload size")
412
+ return path.read_bytes()
413
+
414
+
415
+ def _parse_json_list(value: str, *, field: str) -> list[Any]:
416
+ """Decode one JSON array."""
417
+ result = _decode_json(value, field=field)
418
+ if not isinstance(result, list):
419
+ raise CLIInputError(f"{field} must be a JSON array")
420
+ return result
421
+
422
+
423
+ def _validate_inputs(spec: OperationSpec, args: argparse.Namespace) -> None:
424
+ """Validate scalars and decode JSON arguments before client creation."""
425
+ for name in (*spec["positional"], *spec["required"]):
426
+ if name == "file":
427
+ continue
428
+ value = getattr(args, name, None)
429
+ if isinstance(value, str):
430
+ _required_text(value, field=name)
431
+ for name in spec["json_args"]:
432
+ value = getattr(args, name, None)
433
+ if value is not None:
434
+ setattr(args, name, _parse_json_object(value, field=name))
435
+ for name in spec["list_json_args"]:
436
+ value = getattr(args, name, None)
437
+ if value is not None:
438
+ setattr(args, name, _parse_json_list(value, field=name))
439
+
440
+
441
+ def _model_to_dict(value: Any) -> Any:
442
+ """Convert SDK models to JSON-compatible values."""
443
+ if value is None:
444
+ return None
445
+ if isinstance(value, list):
446
+ return [_model_to_dict(item) for item in value]
447
+ if isinstance(value, dict):
448
+ return {key: _model_to_dict(item) for key, item in value.items()}
449
+ for method_name in ("to_dict", "model_dump", "dict"):
450
+ method = getattr(value, method_name, None)
451
+ if callable(method):
452
+ result = method()
453
+ if isinstance(result, dict):
454
+ return {key: _model_to_dict(item) for key, item in result.items()}
455
+ return value
456
+
457
+
458
+ def _build_kwargs(
459
+ spec: OperationSpec, args: argparse.Namespace, timeout: int
460
+ ) -> dict[str, Any]:
461
+ """Build SDK keyword arguments, omitting absent optional values.
462
+
463
+ ``--file`` on upload/upload_media is consumed separately and never
464
+ forwarded. ``--output`` on download operations is consumed by the
465
+ BinaryDownloadHandler path and never forwarded.
466
+ """
467
+ kwargs: dict[str, Any] = {}
468
+ for name in (*spec["required"], *spec["optional"]):
469
+ if name in {"file", "output"}:
470
+ continue
471
+ value = getattr(args, name, None)
472
+ if value is not None:
473
+ kwargs[name] = value
474
+ kwargs["request_timeout"] = timeout
475
+ return kwargs
476
+
477
+
478
+ async def _invoke(
479
+ spec: OperationSpec,
480
+ client: Any,
481
+ args: argparse.Namespace,
482
+ timeout: int,
483
+ ) -> Any:
484
+ """Invoke one non-downloaded SDK operation."""
485
+ positional = [getattr(args, name) for name in spec["positional"]]
486
+ if (spec["resource"], spec["operation"]) in _UPLOAD_OPS:
487
+ positional.append(getattr(args, "_file_bytes"))
488
+ return await getattr(client, spec["method"])(
489
+ *positional, **_build_kwargs(spec, args, timeout)
490
+ )
491
+
492
+
493
+ async def _download_operation(
494
+ spec: OperationSpec,
495
+ client: Any,
496
+ args: argparse.Namespace,
497
+ timeout: int,
498
+ cfg: ConfigLoader,
499
+ ) -> dict[str, Any]:
500
+ """Stream one bounded media download through BinaryDownloadHandler."""
501
+ method = getattr(getattr(client, "with_streaming_response"), spec["method"])
502
+ positional = [getattr(args, name) for name in spec["positional"]]
503
+ kwargs: dict[str, Any] = {}
504
+ for name in spec["optional"]:
505
+ if name == "output":
506
+ continue
507
+ value = getattr(args, name, None)
508
+ if value is not None:
509
+ kwargs[name] = value
510
+ response_context = method(*positional, request_timeout=timeout, **kwargs)
511
+ async with response_context as response:
512
+ result = await BinaryDownloadHandler(config=cfg).save(
513
+ response.aiter_bytes(),
514
+ original_filename=getattr(args, "output", None),
515
+ namespace="media_sets",
516
+ operation=f"{spec['resource']}.{spec['method']}",
517
+ content_length=None,
518
+ content_encoding=None,
519
+ mime_type=None,
520
+ )
521
+ return result.to_dict()
522
+
523
+
524
+ def _serialize_error(exception: BaseException) -> int:
525
+ """Write one privacy-safe ADR-001 error envelope to stdout."""
526
+ serializer = ErrorSerializer()
527
+ status = sdk_http_status(exception)
528
+ declared = getattr(exception, "exit_code", None)
529
+ sdk_exit = sdk_exception_exit_code(exception)
530
+ if isinstance(declared, int) and 1 <= declared <= 9:
531
+ exit_code = declared
532
+ elif sdk_exit is not None:
533
+ exit_code = sdk_exit
534
+ elif status == 401:
535
+ exit_code = EXIT_AUTH
536
+ elif status == 403:
537
+ exit_code = EXIT_PERMISSION_DENIED
538
+ elif status == 404:
539
+ exit_code = EXIT_NOT_FOUND
540
+ elif status == 429:
541
+ exit_code = EXIT_RATE_LIMIT
542
+ elif isinstance(exception, ConfigurationError):
543
+ exit_code = EXIT_CONFIGURATION
544
+ elif isinstance(exception, (TypeError, ValueError)):
545
+ exit_code = EXIT_USER_INPUT
546
+ elif isinstance(exception, TimeoutError):
547
+ exit_code = EXIT_TIMEOUT
548
+ else:
549
+ exit_code = EXIT_SERVER_ERROR
550
+
551
+ safe = isinstance(
552
+ exception,
553
+ (CLIInputError, ConfigurationError, TimeoutError),
554
+ ) or declared in {EXIT_ACCESS_CONTROL, EXIT_CONFIGURATION}
555
+ message = str(exception) if safe else "Media Sets operation failed"
556
+ envelope = serializer.create_error_envelope(
557
+ exit_code, message, type(exception).__name__, serializer.call_id
558
+ )
559
+ if isinstance(status, int):
560
+ envelope["http_status"] = status
561
+ sys.stdout.write(json.dumps(envelope, default=str) + "\n")
562
+ sys.stdout.flush()
563
+ logger.error(
564
+ "Media Sets operation failed",
565
+ extra={
566
+ "call_id": serializer.call_id,
567
+ "exception_type": type(exception).__name__,
568
+ "http_status": status,
569
+ },
570
+ )
571
+ return exit_code
572
+
573
+
574
+ async def main() -> int:
575
+ """Run one stateless Foundry Media Sets invocation."""
576
+ parser = build_parser()
577
+ try:
578
+ args = parser.parse_args()
579
+ if not args.resource or not getattr(args, "operation", None):
580
+ raise CLIInputError("a Media Sets operation is required")
581
+ except CLIInputError as exc:
582
+ return _serialize_error(exc)
583
+
584
+ try:
585
+ cfg = ConfigLoader()
586
+ cfg.load()
587
+ LogSetup.configure(log_level=cfg.log_level)
588
+
589
+ resource = args.resource.replace("-", "_")
590
+ operation = args.operation.replace("-", "_")
591
+ spec = _spec_for(resource, operation)
592
+ _validate_inputs(spec, args)
593
+ timeout = _validate_timeout(
594
+ args.timeout if args.timeout is not None else cfg.timeout_s
595
+ )
596
+ AccessControlGuard(
597
+ cfg,
598
+ "MEDIA_SETS",
599
+ metadata_allowlist_path=str(_METADATA_ALLOWLIST_PATH),
600
+ ).check(resource, operation)
601
+
602
+ if (resource, operation) in _UPLOAD_OPS:
603
+ # Bounded file read after the access-control decision and before
604
+ # any client construction (DESIGN-018 binary handling contract).
605
+ file_bytes = _read_file_bounded(
606
+ getattr(args, "file", "") or "", field="file"
607
+ )
608
+ setattr(args, "_file_bytes", file_bytes)
609
+
610
+ factory = AsyncClientFactory()
611
+ with factory.invocation_scope(cfg, include_attribution=True):
612
+ root_client = factory.create(cfg, include_attribution=True)
613
+ client = _get_client(root_client, spec["client_path"])
614
+ retry_handler = RetryHandler(timeout_s=timeout)
615
+ if (resource, operation) in DOWNLOAD_OPS:
616
+ result = await retry_handler.execute(
617
+ _download_operation, spec, client, args, timeout, cfg
618
+ )
619
+ else:
620
+ result = await retry_handler.execute(
621
+ _invoke, spec, client, args, timeout
622
+ )
623
+
624
+ formatter = OutputFormatter(format_setting=args.format, pretty=args.pretty)
625
+ print(formatter.format(_model_to_dict(result)))
626
+ return EXIT_SUCCESS
627
+ except AccessControlError as exc:
628
+ ErrorSerializer().serialize(exc)
629
+ return EXIT_ACCESS_CONTROL
630
+ except asyncio.CancelledError:
631
+ return _serialize_error(TimeoutError("Operation cancelled"))
632
+ except Exception as exc:
633
+ return _serialize_error(exc)
634
+
635
+
636
+ def console_main() -> int:
637
+ """Run the async CLI through one event-loop boundary."""
638
+ return asyncio.run(main())
639
+
640
+
641
+ if __name__ == "__main__":
642
+ raise SystemExit(console_main())
@@ -0,0 +1,5 @@
1
+ """Foundry Models command-line namespace."""
2
+
3
+ from pal_found_cli.models.scripts.pal_found_models_cli import build_parser, console_main, main
4
+
5
+ __all__ = ["build_parser", "console_main", "main"]
@@ -0,0 +1,27 @@
1
+ # Models metadata allow-list
2
+
3
+ | SDK Path | Status | Rationale |
4
+ | --- | --- | --- |
5
+ | `models.experiment.get` | PERMITTED | Experiment metadata |
6
+ | `models.experiment.search` | PERMITTED | Experiment metadata search |
7
+ | `models.experiment_artifact_table.json` | BLOCKED | Streams table content bytes |
8
+ | `models.experiment_artifact_table.parquet` | BLOCKED | Streams table content bytes |
9
+ | `models.experiment_series.json` | BLOCKED | Streams series content bytes |
10
+ | `models.experiment_series.parquet` | BLOCKED | Streams series content bytes |
11
+ | `models.live_deployment.transform_json` | BLOCKED | Executes model inference |
12
+ | `models.model.create` | BLOCKED | Creates a model |
13
+ | `models.model.get` | PERMITTED | Model metadata |
14
+ | `models.model.promote_version` | BLOCKED | Mutates model versions |
15
+ | `models.model_studio.create` | BLOCKED | Creates a Model Studio |
16
+ | `models.model_studio.get` | PERMITTED | Model Studio metadata |
17
+ | `models.model_studio.launch` | BLOCKED | Launches billable training runs |
18
+ | `models.model_studio_config_version.create` | BLOCKED | Creates a configuration version |
19
+ | `models.model_studio_config_version.get` | PERMITTED | Configuration version metadata |
20
+ | `models.model_studio_config_version.latest` | PERMITTED | Configuration version metadata |
21
+ | `models.model_studio_config_version.list` | PERMITTED | Configuration version metadata |
22
+ | `models.model_studio_run.list` | PERMITTED | Run metadata |
23
+ | `models.model_studio_trainer.get` | PERMITTED | Trainer metadata |
24
+ | `models.model_studio_trainer.list` | PERMITTED | Trainer metadata |
25
+ | `models.model_version.create` | BLOCKED | Creates a model version |
26
+ | `models.model_version.get` | PERMITTED | Model version metadata |
27
+ | `models.model_version.list` | PERMITTED | Model version metadata |
@@ -0,0 +1 @@
1
+ """Foundry Models CLI entry points."""