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,547 @@
1
+ #!/usr/bin/env python3
2
+ """Foundry Third-Party Applications CLI - 9 canonical Third-Party Applications API v2 operations.
3
+
4
+ Exposes exactly 9 public ``foundry_sdk.v2.third_party_applications``
5
+ operations as CLI subcommands across the ThirdPartyApplication, Website,
6
+ and Version client paths:
7
+
8
+ - third-party-application (1): get
9
+ - website (3): deploy, get, undeploy
10
+ - version (5): delete, get, list, upload, upload-snapshot
11
+
12
+ ``version list`` returns an SDK ``ResourceIterator`` and implements the
13
+ ADR-003 cursor-paged pattern via PaginationHelper with
14
+ ``--page-size``/``--page-token``/``--all``/``--max-pages``.
15
+
16
+ ``version upload`` and ``version upload-snapshot`` accept the Website build
17
+ as a zip file. The CLI reads ``--file`` with a bounded read (16 MiB) after
18
+ the access-control decision and before client construction. The ``--file``
19
+ flag is consumed by the CLI and never forwarded; ``--version`` is passed as
20
+ the SDK ``version`` query parameter. Snapshot versions are automatically
21
+ deleted after two days (disclosed in the skill documentation).
22
+
23
+ Usage: pal-found-third-party-applications <resource> <operation> [options]
24
+ Output: JSON/TOON on stdout, NDJSON diagnostics on stderr (ADR-004, ADR-005).
25
+ Exit codes per ADR-001 taxonomy.
26
+ Access control: AccessControlGuard before client construction and file
27
+ effects (SRS 4.2, ADR-007). Retry: transient conditions only per ADR-002.
28
+ Tracing: SDK-native B3 via invocation_scope; include_attribution=False
29
+ (DESIGN-021; this namespace is outside FR-ATTR-4 scope).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import argparse
35
+ import asyncio
36
+ import json
37
+ import logging
38
+ import sys
39
+ from pathlib import Path
40
+ from typing import Any, NoReturn
41
+
42
+ from pal_found_cli.common.access_control_guard import (
43
+ AccessControlError,
44
+ AccessControlGuard,
45
+ )
46
+ from pal_found_cli.common.async_client_factory import AsyncClientFactory
47
+ from pal_found_cli.common.config_loader import ConfigLoader, ConfigurationError
48
+ from pal_found_cli.common.error_serializer import (
49
+ EXIT_ACCESS_CONTROL,
50
+ EXIT_AUTH,
51
+ EXIT_CONFIGURATION,
52
+ EXIT_NOT_FOUND,
53
+ EXIT_PERMISSION_DENIED,
54
+ EXIT_RATE_LIMIT,
55
+ EXIT_SERVER_ERROR,
56
+ EXIT_SUCCESS,
57
+ EXIT_TIMEOUT,
58
+ EXIT_USER_INPUT,
59
+ ErrorSerializer,
60
+ )
61
+ from pal_found_cli.common.log_setup import LogSetup
62
+ from pal_found_cli.common.output_formatter import OutputFormatter
63
+ from pal_found_cli.common.pagination_helper import HARD_MAX_BATCH_PAGES, PaginationHelper
64
+ from pal_found_cli.common.retry import RetryHandler
65
+ from pal_found_cli.common.sdk_error_utils import sdk_exception_exit_code, sdk_http_status
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+ OperationSpec = dict[str, Any]
70
+
71
+ _METADATA_ALLOWLIST_PATH = Path(__file__).resolve().parents[1] / "metadata-allow-list.md"
72
+
73
+ # The single cursor-paged command (DESIGN-021 paging contract). The SDK list
74
+ # method returns core.ResourceIterator and accepts page_size/page_token.
75
+ PAGINATED_OPS: frozenset[tuple[str, str]] = frozenset({("version", "list")})
76
+
77
+ # Bounded binary upload size cap for Website zip builds (aligned with the
78
+ # standard upload bound used across namespaces, DESIGN-021 binary contract).
79
+ _UPLOAD_MAX_BYTES = 16 * 1024 * 1024
80
+
81
+
82
+ class CLIInputError(ValueError):
83
+ """A locally generated input error whose message never echoes values."""
84
+
85
+ exit_code = EXIT_USER_INPUT
86
+
87
+
88
+ class _ArgumentParser(argparse.ArgumentParser):
89
+ """Raise parser failures into the structured error boundary."""
90
+
91
+ def error(self, message: str) -> NoReturn:
92
+ raise CLIInputError("Invalid command input")
93
+
94
+
95
+ def _op(
96
+ resource: str,
97
+ operation: str,
98
+ client_path: tuple[str, ...],
99
+ method: str,
100
+ positional: tuple[str, ...] = (),
101
+ required: tuple[str, ...] = (),
102
+ optional: tuple[str, ...] = (),
103
+ *,
104
+ json_args: frozenset[str] = frozenset(),
105
+ list_json_args: frozenset[str] = frozenset(),
106
+ ) -> OperationSpec:
107
+ """Build one operation specification entry."""
108
+ return {
109
+ "resource": resource,
110
+ "operation": operation,
111
+ "client_path": client_path,
112
+ "method": method,
113
+ "positional": positional,
114
+ "required": required,
115
+ "optional": optional,
116
+ "json_args": json_args,
117
+ "list_json_args": list_json_args,
118
+ }
119
+
120
+
121
+ OP_SPECS: tuple[OperationSpec, ...] = (
122
+ _op(
123
+ "third_party_application",
124
+ "get",
125
+ ("ThirdPartyApplication",),
126
+ "get",
127
+ ("third_party_application_rid",),
128
+ ),
129
+ _op(
130
+ "version",
131
+ "delete",
132
+ ("Website", "Version"),
133
+ "delete",
134
+ ("third_party_application_rid", "version_version"),
135
+ ),
136
+ _op(
137
+ "version",
138
+ "get",
139
+ ("Website", "Version"),
140
+ "get",
141
+ ("third_party_application_rid", "version_version"),
142
+ ),
143
+ _op(
144
+ "version",
145
+ "list",
146
+ ("Website", "Version"),
147
+ "list",
148
+ ("third_party_application_rid",),
149
+ ),
150
+ _op(
151
+ "version",
152
+ "upload",
153
+ ("Website", "Version"),
154
+ "upload",
155
+ ("third_party_application_rid",),
156
+ required=("version", "file"),
157
+ ),
158
+ _op(
159
+ "version",
160
+ "upload_snapshot",
161
+ ("Website", "Version"),
162
+ "upload_snapshot",
163
+ ("third_party_application_rid",),
164
+ required=("version", "file"),
165
+ optional=("snapshot_identifier",),
166
+ ),
167
+ _op(
168
+ "website",
169
+ "deploy",
170
+ ("Website",),
171
+ "deploy",
172
+ ("third_party_application_rid",),
173
+ required=("version",),
174
+ ),
175
+ _op(
176
+ "website",
177
+ "get",
178
+ ("Website",),
179
+ "get",
180
+ ("third_party_application_rid",),
181
+ ),
182
+ _op(
183
+ "website",
184
+ "undeploy",
185
+ ("Website",),
186
+ "undeploy",
187
+ ("third_party_application_rid",),
188
+ ),
189
+ )
190
+
191
+ OPERATION_BY_RESOURCE: dict[str, dict[str, OperationSpec]] = {}
192
+ for _spec in OP_SPECS:
193
+ OPERATION_BY_RESOURCE.setdefault(_spec["resource"], {})[_spec["operation"]] = _spec
194
+
195
+
196
+ def _common_parser(*, paginated: bool) -> _ArgumentParser:
197
+ """Build operation-level common options."""
198
+ parser = _ArgumentParser(add_help=False)
199
+ parser.add_argument("--timeout", type=int, default=None)
200
+ parser.add_argument("--format", choices=("json", "toon", "auto"), default="auto")
201
+ parser.add_argument("--pretty", action="store_true")
202
+ if paginated:
203
+ parser.add_argument("--page-size", type=int, default=None, dest="page_size")
204
+ parser.add_argument("--page-token", default=None, dest="page_token")
205
+ parser.add_argument("--all", action="store_true", dest="all_pages")
206
+ parser.add_argument("--max-pages", type=int, default=None, dest="max_pages")
207
+ return parser
208
+
209
+
210
+ def _kebab(name: str) -> str:
211
+ """Convert snake_case names to CLI kebab-case."""
212
+ return name.replace("_", "-")
213
+
214
+
215
+ def _add_kwarg(
216
+ parser: argparse.ArgumentParser, arg_name: str, *, required: bool
217
+ ) -> None:
218
+ """Add one named operation argument."""
219
+ flag = "--" + _kebab(arg_name)
220
+ kwargs: dict[str, Any] = {"required": required, "default": None, "dest": arg_name}
221
+ parser.add_argument(flag, **kwargs)
222
+
223
+
224
+ def build_parser() -> argparse.ArgumentParser:
225
+ """Build argparse parser with all 9 Third-Party Applications operations."""
226
+ parser = _ArgumentParser(
227
+ prog="pal-found-third-party-applications",
228
+ description=(
229
+ "Foundry Third-Party Applications CLI - "
230
+ "9 Third-Party Applications API v2 operations"
231
+ ),
232
+ )
233
+ subparsers = parser.add_subparsers(dest="resource", help="Resource type")
234
+
235
+ for resource in sorted(OPERATION_BY_RESOURCE):
236
+ res_parser = subparsers.add_parser(_kebab(resource))
237
+ op_sub = res_parser.add_subparsers(dest="operation")
238
+ for operation, spec in sorted(OPERATION_BY_RESOURCE[resource].items()):
239
+ is_paginated = (resource, operation) in PAGINATED_OPS
240
+ op_parser = op_sub.add_parser(
241
+ _kebab(operation), parents=[_common_parser(paginated=is_paginated)]
242
+ )
243
+ for arg_name in spec["positional"]:
244
+ op_parser.add_argument(arg_name)
245
+ for arg_name in spec["required"]:
246
+ _add_kwarg(op_parser, arg_name, required=True)
247
+ for arg_name in spec["optional"]:
248
+ if arg_name in {"page_size", "page_token"} and is_paginated:
249
+ continue
250
+ _add_kwarg(op_parser, arg_name, required=False)
251
+
252
+ return parser
253
+
254
+
255
+ def _spec_for(resource: str, operation: str) -> OperationSpec:
256
+ """Return one catalog operation specification."""
257
+ try:
258
+ return OPERATION_BY_RESOURCE[resource][operation]
259
+ except KeyError as exc:
260
+ raise CLIInputError(f"Unknown operation: {resource}.{operation}") from exc
261
+
262
+
263
+ def _get_client(root_client: Any, client_path: tuple[str, ...]) -> Any:
264
+ """Resolve the exact public nested Third-Party Applications SDK resource."""
265
+ client = root_client.third_party_applications
266
+ for attribute in client_path:
267
+ client = getattr(client, attribute)
268
+ return client
269
+
270
+
271
+ def _required_text(value: Any, *, field: str) -> str:
272
+ """Validate a non-empty string without echoing its value."""
273
+ if not isinstance(value, str) or not value.strip():
274
+ raise CLIInputError(f"{field} must not be empty")
275
+ return value
276
+
277
+
278
+ def _validate_timeout(value: int) -> int:
279
+ """Validate the effective per-attempt timeout from ADR-002."""
280
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 3600:
281
+ raise CLIInputError("timeout must be between 1 and 3600 seconds")
282
+ return value
283
+
284
+
285
+ def _read_file_bounded(path_value: str, *, field: str) -> bytes:
286
+ """Read a local zip file for a bounded binary upload."""
287
+ path = Path(path_value)
288
+ if not path.is_file():
289
+ raise CLIInputError(f"{field} must reference an existing file")
290
+ size = path.stat().st_size
291
+ if size > _UPLOAD_MAX_BYTES:
292
+ raise CLIInputError(f"{field} exceeds the maximum upload size")
293
+ return path.read_bytes()
294
+
295
+
296
+ def _validate_inputs(spec: OperationSpec, args: argparse.Namespace) -> None:
297
+ """Validate scalars before client creation."""
298
+ for name in (*spec["positional"], *spec["required"]):
299
+ value = getattr(args, name, None)
300
+ if isinstance(value, str):
301
+ _required_text(value, field=name)
302
+
303
+
304
+ def _model_to_dict(value: Any) -> Any:
305
+ """Convert SDK models to JSON-compatible values."""
306
+ if value is None:
307
+ return None
308
+ if isinstance(value, list):
309
+ return [_model_to_dict(item) for item in value]
310
+ if isinstance(value, dict):
311
+ return {key: _model_to_dict(item) for key, item in value.items()}
312
+ for method_name in ("to_dict", "model_dump", "dict"):
313
+ method = getattr(value, method_name, None)
314
+ if callable(method):
315
+ result = method()
316
+ if isinstance(result, dict):
317
+ return {key: _model_to_dict(item) for key, item in result.items()}
318
+ return value
319
+
320
+
321
+ def _build_kwargs(
322
+ spec: OperationSpec, args: argparse.Namespace, timeout: int
323
+ ) -> dict[str, Any]:
324
+ """Build SDK keyword arguments, omitting absent optional values.
325
+
326
+ For cursor-paged operations the page_size/page_token come from the
327
+ PaginationHelper path and are intentionally omitted here. ``--file``
328
+ on upload/upload_snapshot is consumed separately and never forwarded.
329
+ """
330
+ is_paginated = (spec["resource"], spec["operation"]) in PAGINATED_OPS
331
+ kwargs: dict[str, Any] = {}
332
+ for name in (*spec["required"], *spec["optional"]):
333
+ if name == "file":
334
+ continue
335
+ if name in {"page_size", "page_token"} and is_paginated:
336
+ continue
337
+ value = getattr(args, name, None)
338
+ if value is not None:
339
+ kwargs[name] = value
340
+ kwargs["request_timeout"] = timeout
341
+ return kwargs
342
+
343
+
344
+ async def _invoke(
345
+ spec: OperationSpec,
346
+ client: Any,
347
+ args: argparse.Namespace,
348
+ timeout: int,
349
+ ) -> Any:
350
+ """Invoke one non-paginated SDK operation."""
351
+ positional = [getattr(args, name) for name in spec["positional"]]
352
+ if spec["method"] in {"upload", "upload_snapshot"}:
353
+ positional.append(getattr(args, "_file_bytes"))
354
+ return await getattr(client, spec["method"])(
355
+ *positional, **_build_kwargs(spec, args, timeout)
356
+ )
357
+
358
+
359
+ async def _fetch_raw_page(
360
+ method: Any,
361
+ *,
362
+ page_size: int,
363
+ page_token: str | None,
364
+ request_timeout: int | None,
365
+ positional: tuple[Any, ...],
366
+ extra_kwargs: dict[str, Any],
367
+ ) -> dict[str, Any]:
368
+ """Fetch exactly one decoded SDK page via with_raw_response."""
369
+ raw_response = await method(
370
+ *positional,
371
+ page_size=page_size,
372
+ page_token=page_token,
373
+ request_timeout=request_timeout,
374
+ **extra_kwargs,
375
+ )
376
+ page = raw_response.decode()
377
+ return {
378
+ "items": list(getattr(page, "data", None) or []),
379
+ "next_page_token": getattr(page, "next_page_token", None),
380
+ }
381
+
382
+
383
+ def _resolve_pagination_flags(args: argparse.Namespace) -> int:
384
+ """Resolve the effective page batch size from --all/--max-pages."""
385
+ if getattr(args, "all_pages", False):
386
+ return HARD_MAX_BATCH_PAGES
387
+ return getattr(args, "max_pages", None) or 1
388
+
389
+
390
+ async def _paginate_operation(
391
+ spec: OperationSpec,
392
+ client: Any,
393
+ args: argparse.Namespace,
394
+ timeout: int,
395
+ ) -> tuple[list[Any], PaginationHelper]:
396
+ """Collect a bounded number of actual server pages."""
397
+ raw_method = getattr(getattr(client, "with_raw_response"), spec["method"])
398
+ batch_pages = _resolve_pagination_flags(args)
399
+ helper = PaginationHelper(
400
+ page_size=getattr(args, "page_size", None),
401
+ page_token=getattr(args, "page_token", None),
402
+ batch_pages=batch_pages,
403
+ )
404
+ positional = tuple(getattr(args, name) for name in spec["positional"])
405
+ extra_kwargs: dict[str, Any] = {}
406
+ for name in spec["optional"]:
407
+ if name in {"page_size", "page_token"}:
408
+ continue
409
+ value = getattr(args, name, None)
410
+ if value is not None:
411
+ extra_kwargs[name] = value
412
+
413
+ async def fetch_page(**page_kwargs: Any) -> dict[str, Any]:
414
+ return await _fetch_raw_page(
415
+ raw_method,
416
+ page_size=page_kwargs["page_size"],
417
+ page_token=page_kwargs.get("page_token"),
418
+ request_timeout=timeout,
419
+ positional=positional,
420
+ extra_kwargs=extra_kwargs,
421
+ )
422
+
423
+ return await helper.paginate(fetch_page), helper
424
+
425
+
426
+ def _serialize_error(exception: BaseException) -> int:
427
+ """Write one privacy-safe ADR-001 error envelope to stdout."""
428
+ serializer = ErrorSerializer()
429
+ status = sdk_http_status(exception)
430
+ declared = getattr(exception, "exit_code", None)
431
+ sdk_exit = sdk_exception_exit_code(exception)
432
+ if isinstance(declared, int) and 1 <= declared <= 9:
433
+ exit_code = declared
434
+ elif sdk_exit is not None:
435
+ exit_code = sdk_exit
436
+ elif status == 401:
437
+ exit_code = EXIT_AUTH
438
+ elif status == 403:
439
+ exit_code = EXIT_PERMISSION_DENIED
440
+ elif status == 404:
441
+ exit_code = EXIT_NOT_FOUND
442
+ elif status == 429:
443
+ exit_code = EXIT_RATE_LIMIT
444
+ elif isinstance(exception, ConfigurationError):
445
+ exit_code = EXIT_CONFIGURATION
446
+ elif isinstance(exception, (TypeError, ValueError)):
447
+ exit_code = EXIT_USER_INPUT
448
+ elif isinstance(exception, TimeoutError):
449
+ exit_code = EXIT_TIMEOUT
450
+ else:
451
+ exit_code = EXIT_SERVER_ERROR
452
+
453
+ safe = isinstance(
454
+ exception,
455
+ (CLIInputError, ConfigurationError, TimeoutError),
456
+ ) or declared in {EXIT_ACCESS_CONTROL, EXIT_CONFIGURATION}
457
+ message = str(exception) if safe else "Third-Party Applications operation failed"
458
+ envelope = serializer.create_error_envelope(
459
+ exit_code, message, type(exception).__name__, serializer.call_id
460
+ )
461
+ if isinstance(status, int):
462
+ envelope["http_status"] = status
463
+ sys.stdout.write(json.dumps(envelope, default=str) + "\n")
464
+ sys.stdout.flush()
465
+ logger.error(
466
+ "Third-Party Applications operation failed",
467
+ extra={
468
+ "call_id": serializer.call_id,
469
+ "exception_type": type(exception).__name__,
470
+ "http_status": status,
471
+ },
472
+ )
473
+ return exit_code
474
+
475
+
476
+ async def main() -> int:
477
+ """Run one stateless Foundry Third-Party Applications invocation."""
478
+ parser = build_parser()
479
+ try:
480
+ args = parser.parse_args()
481
+ if not args.resource or not getattr(args, "operation", None):
482
+ raise CLIInputError("a Third-Party Applications operation is required")
483
+ except CLIInputError as exc:
484
+ return _serialize_error(exc)
485
+
486
+ try:
487
+ cfg = ConfigLoader()
488
+ cfg.load()
489
+ LogSetup.configure(log_level=cfg.log_level)
490
+
491
+ resource = args.resource.replace("-", "_")
492
+ operation = args.operation.replace("-", "_")
493
+ spec = _spec_for(resource, operation)
494
+ _validate_inputs(spec, args)
495
+ timeout = _validate_timeout(
496
+ args.timeout if args.timeout is not None else cfg.timeout_s
497
+ )
498
+ AccessControlGuard(
499
+ cfg,
500
+ "THIRD_PARTY_APPLICATIONS",
501
+ metadata_allowlist_path=str(_METADATA_ALLOWLIST_PATH),
502
+ ).check(resource, operation)
503
+
504
+ if spec["method"] in {"upload", "upload_snapshot"}:
505
+ # Bounded file read after the access-control decision and before
506
+ # any client construction (DESIGN-021 binary handling contract).
507
+ file_bytes = _read_file_bounded(
508
+ getattr(args, "file", "") or "", field="file"
509
+ )
510
+ setattr(args, "_file_bytes", file_bytes)
511
+
512
+ factory = AsyncClientFactory()
513
+ helper: PaginationHelper | None = None
514
+ with factory.invocation_scope(cfg, include_attribution=False):
515
+ root_client = factory.create(cfg, include_attribution=False)
516
+ client = _get_client(root_client, spec["client_path"])
517
+ retry_handler = RetryHandler(timeout_s=timeout)
518
+ if (resource, operation) in PAGINATED_OPS:
519
+ result, helper = await retry_handler.execute(
520
+ _paginate_operation, spec, client, args, timeout
521
+ )
522
+ else:
523
+ result = await retry_handler.execute(
524
+ _invoke, spec, client, args, timeout
525
+ )
526
+
527
+ formatter = OutputFormatter(format_setting=args.format, pretty=args.pretty)
528
+ print(formatter.format(_model_to_dict(result)))
529
+ if helper is not None:
530
+ helper.emit_metadata()
531
+ return EXIT_SUCCESS
532
+ except AccessControlError as exc:
533
+ ErrorSerializer().serialize(exc)
534
+ return EXIT_ACCESS_CONTROL
535
+ except asyncio.CancelledError:
536
+ return _serialize_error(TimeoutError("Operation cancelled"))
537
+ except Exception as exc:
538
+ return _serialize_error(exc)
539
+
540
+
541
+ def console_main() -> int:
542
+ """Run the async CLI through one event-loop boundary."""
543
+ return asyncio.run(main())
544
+
545
+
546
+ if __name__ == "__main__":
547
+ raise SystemExit(console_main())
@@ -0,0 +1,9 @@
1
+ """Foundry Widgets CLI package."""
2
+
3
+ from pal_found_cli.widgets.scripts.pal_found_widgets_cli import ( # noqa: E402
4
+ build_parser,
5
+ console_main,
6
+ main,
7
+ )
8
+
9
+ __all__ = ["build_parser", "console_main", "main"]
@@ -0,0 +1,12 @@
1
+ # Widgets metadata allow-list
2
+
3
+ | SDK Path | Status | Rationale |
4
+ | --- | --- | --- |
5
+ | `widgets.dev_mode_settings.enable` | BLOCKED | Enables dev mode (token-scoped write) |
6
+ | `widgets.dev_mode_settings.set_widget_set_by_id` | BLOCKED | Mutates dev mode settings by widget ID |
7
+ | `widgets.release.delete` | BLOCKED | Deletes a widget set release |
8
+ | `widgets.release.get` | PERMITTED | Widget set release metadata |
9
+ | `widgets.release.list` | PERMITTED | Widget set release list metadata |
10
+ | `widgets.repository.get` | PERMITTED | Repository metadata |
11
+ | `widgets.repository.publish` | BLOCKED | Publishes a widget set release zip |
12
+ | `widgets.widget_set.get` | PERMITTED | Widget set metadata |
@@ -0,0 +1 @@
1
+ """Foundry Widgets CLI scripts package."""