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