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,9 @@
1
+ """Foundry Orchestration command-line namespace."""
2
+
3
+ from pal_found_cli.orchestration.scripts.pal_found_orchestration_cli import (
4
+ build_parser,
5
+ console_main,
6
+ main,
7
+ )
8
+
9
+ __all__ = ["build_parser", "console_main", "main"]
@@ -0,0 +1,24 @@
1
+ # Orchestration metadata allow-list
2
+
3
+ | SDK Path | Status | Rationale |
4
+ | --- | --- | --- |
5
+ | `orchestration.build.cancel` | BLOCKED | Mutates build state |
6
+ | `orchestration.build.create` | BLOCKED | Creates builds |
7
+ | `orchestration.build.get` | PERMITTED | Build metadata |
8
+ | `orchestration.build.get_batch` | PERMITTED | Build metadata |
9
+ | `orchestration.build.jobs` | PERMITTED | Job metadata |
10
+ | `orchestration.build.search` | PERMITTED | Build metadata search |
11
+ | `orchestration.job.get` | PERMITTED | Job metadata |
12
+ | `orchestration.job.get_batch` | PERMITTED | Job metadata |
13
+ | `orchestration.schedule.create` | BLOCKED | Creates schedules |
14
+ | `orchestration.schedule.delete` | BLOCKED | Deletes schedules |
15
+ | `orchestration.schedule.get` | PERMITTED | Schedule metadata |
16
+ | `orchestration.schedule.get_affected_resources` | PERMITTED | Affected resource metadata |
17
+ | `orchestration.schedule.get_batch` | PERMITTED | Schedule metadata |
18
+ | `orchestration.schedule.pause` | BLOCKED | Mutates schedule state |
19
+ | `orchestration.schedule.replace` | BLOCKED | Replaces schedules |
20
+ | `orchestration.schedule.run` | BLOCKED | Triggers schedule runs |
21
+ | `orchestration.schedule.runs` | PERMITTED | Run metadata |
22
+ | `orchestration.schedule.unpause` | BLOCKED | Mutates schedule state |
23
+ | `orchestration.schedule_version.get` | PERMITTED | Schedule version metadata |
24
+ | `orchestration.schedule_version.schedule` | PERMITTED | Schedule metadata |
@@ -0,0 +1 @@
1
+ """Foundry Orchestration CLI entry points."""
@@ -0,0 +1,640 @@
1
+ #!/usr/bin/env python3
2
+ """Foundry Orchestration CLI - 20 canonical Orchestration API v2 operations.
3
+
4
+ Exposes exactly 20 public ``foundry_sdk.v2.orchestration`` operations as CLI
5
+ subcommands across Build, Job, Schedule, and ScheduleVersion client paths:
6
+
7
+ - build (6): cancel, create, get, get-batch, jobs, search
8
+ - job (2): get, get-batch
9
+ - schedule (10): create, delete, get, get-affected-resources, get-batch,
10
+ pause, replace, run, runs, unpause
11
+ - schedule-version (2): get, schedule
12
+
13
+ The ScheduleRun sub-client exists in the SDK but exposes no public methods;
14
+ it must not appear in ``OP_SPECS``.
15
+
16
+ Usage: pal-found-orchestration <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 (SRS 4.2,
20
+ ADR-007). Retry: transient conditions only per ADR-002.
21
+ Tracing: SDK-native B3 via invocation_scope; include_attribution=False.
22
+ No binary downloads: BinaryDownloadHandler is not required.
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.config_loader import ConfigLoader, ConfigurationError
41
+ from pal_found_cli.common.error_serializer import (
42
+ EXIT_ACCESS_CONTROL,
43
+ EXIT_AUTH,
44
+ EXIT_CONFIGURATION,
45
+ EXIT_NOT_FOUND,
46
+ EXIT_PERMISSION_DENIED,
47
+ EXIT_RATE_LIMIT,
48
+ EXIT_SERVER_ERROR,
49
+ EXIT_SUCCESS,
50
+ EXIT_TIMEOUT,
51
+ EXIT_USER_INPUT,
52
+ ErrorSerializer,
53
+ )
54
+ from pal_found_cli.common.log_setup import LogSetup
55
+ from pal_found_cli.common.output_formatter import OutputFormatter
56
+ from pal_found_cli.common.pagination_helper import HARD_MAX_BATCH_PAGES, PaginationHelper
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 = (
65
+ Path(__file__).resolve().parents[1] / "metadata-allow-list.md"
66
+ )
67
+
68
+ # The three cursor-paged commands (DESIGN-014 paging contract). Batch
69
+ # get_batch commands and build search are single-call responses.
70
+ PAGINATED_OPS: frozenset[tuple[str, str]] = frozenset(
71
+ {
72
+ ("build", "jobs"),
73
+ ("build", "search"),
74
+ ("schedule", "runs"),
75
+ }
76
+ )
77
+
78
+ # The eight mutating operations (DESIGN-014 write set).
79
+ WRITE_OPS: frozenset[tuple[str, str]] = frozenset(
80
+ {
81
+ ("build", "cancel"),
82
+ ("build", "create"),
83
+ ("schedule", "create"),
84
+ ("schedule", "delete"),
85
+ ("schedule", "pause"),
86
+ ("schedule", "replace"),
87
+ ("schedule", "run"),
88
+ ("schedule", "unpause"),
89
+ }
90
+ )
91
+
92
+
93
+ class CLIInputError(ValueError):
94
+ """A locally generated input error whose message never echoes values."""
95
+
96
+ exit_code = EXIT_USER_INPUT
97
+
98
+
99
+ class _ArgumentParser(argparse.ArgumentParser):
100
+ """Raise parser failures into the structured error boundary."""
101
+
102
+ def error(self, message: str) -> NoReturn:
103
+ raise CLIInputError("Invalid command input")
104
+
105
+
106
+ def _op(
107
+ resource: str,
108
+ operation: str,
109
+ client_path: tuple[str, ...],
110
+ method: str,
111
+ positional: tuple[str, ...] = (),
112
+ required: tuple[str, ...] = (),
113
+ optional: tuple[str, ...] = (),
114
+ *,
115
+ json_args: frozenset[str] = frozenset(),
116
+ list_json_args: frozenset[str] = frozenset(),
117
+ ) -> OperationSpec:
118
+ """Build one operation specification entry."""
119
+ return {
120
+ "resource": resource,
121
+ "operation": operation,
122
+ "client_path": client_path,
123
+ "method": method,
124
+ "positional": positional,
125
+ "required": required,
126
+ "optional": optional,
127
+ "json_args": json_args,
128
+ "list_json_args": list_json_args,
129
+ }
130
+
131
+
132
+ OP_SPECS: tuple[OperationSpec, ...] = (
133
+ _op("build", "cancel", ("Build",), "cancel", ("build_rid",)),
134
+ _op(
135
+ "build",
136
+ "create",
137
+ ("Build",),
138
+ "create",
139
+ required=("target", "fallback_branches"),
140
+ optional=(
141
+ "abort_on_failure",
142
+ "branch_name",
143
+ "force_build",
144
+ "notifications_enabled",
145
+ "retry_backoff_duration",
146
+ "retry_count",
147
+ ),
148
+ json_args=frozenset({"target", "retry_backoff_duration"}),
149
+ list_json_args=frozenset({"fallback_branches"}),
150
+ ),
151
+ _op("build", "get", ("Build",), "get", ("build_rid",)),
152
+ _op(
153
+ "build",
154
+ "get_batch",
155
+ ("Build",),
156
+ "get_batch",
157
+ required=("build_rids",),
158
+ list_json_args=frozenset({"build_rids"}),
159
+ ),
160
+ _op(
161
+ "build",
162
+ "jobs",
163
+ ("Build",),
164
+ "jobs",
165
+ ("build_rid",),
166
+ optional=("page_size", "page_token"),
167
+ ),
168
+ _op(
169
+ "build",
170
+ "search",
171
+ ("Build",),
172
+ "search",
173
+ optional=("where", "order_by", "page_size", "page_token"),
174
+ json_args=frozenset({"where", "order_by"}),
175
+ ),
176
+ _op("job", "get", ("Job",), "get", ("job_rid",)),
177
+ _op(
178
+ "job",
179
+ "get_batch",
180
+ ("Job",),
181
+ "get_batch",
182
+ required=("job_rids",),
183
+ list_json_args=frozenset({"job_rids"}),
184
+ ),
185
+ _op(
186
+ "schedule",
187
+ "create",
188
+ ("Schedule",),
189
+ "create",
190
+ required=("action", "trigger", "scope_mode"),
191
+ optional=("display_name", "description"),
192
+ json_args=frozenset({"action", "trigger", "scope_mode"}),
193
+ ),
194
+ _op("schedule", "delete", ("Schedule",), "delete", ("schedule_rid",)),
195
+ _op("schedule", "get", ("Schedule",), "get", ("schedule_rid",)),
196
+ _op(
197
+ "schedule",
198
+ "get_affected_resources",
199
+ ("Schedule",),
200
+ "get_affected_resources",
201
+ ("schedule_rid",),
202
+ ),
203
+ _op(
204
+ "schedule",
205
+ "get_batch",
206
+ ("Schedule",),
207
+ "get_batch",
208
+ required=("schedule_rids",),
209
+ list_json_args=frozenset({"schedule_rids"}),
210
+ ),
211
+ _op("schedule", "pause", ("Schedule",), "pause", ("schedule_rid",)),
212
+ _op(
213
+ "schedule",
214
+ "replace",
215
+ ("Schedule",),
216
+ "replace",
217
+ ("schedule_rid",),
218
+ required=("action", "trigger", "scope_mode"),
219
+ optional=("display_name", "description"),
220
+ json_args=frozenset({"action", "trigger", "scope_mode"}),
221
+ ),
222
+ _op("schedule", "run", ("Schedule",), "run", ("schedule_rid",)),
223
+ _op(
224
+ "schedule",
225
+ "runs",
226
+ ("Schedule",),
227
+ "runs",
228
+ ("schedule_rid",),
229
+ optional=("page_size", "page_token"),
230
+ ),
231
+ _op("schedule", "unpause", ("Schedule",), "unpause", ("schedule_rid",)),
232
+ _op(
233
+ "schedule_version",
234
+ "get",
235
+ ("ScheduleVersion",),
236
+ "get",
237
+ ("schedule_version_rid",),
238
+ ),
239
+ _op(
240
+ "schedule_version",
241
+ "schedule",
242
+ ("ScheduleVersion",),
243
+ "schedule",
244
+ ("schedule_version_rid",),
245
+ ),
246
+ )
247
+
248
+ OPERATION_BY_RESOURCE: dict[str, dict[str, OperationSpec]] = {}
249
+ for _spec in OP_SPECS:
250
+ OPERATION_BY_RESOURCE.setdefault(_spec["resource"], {})[_spec["operation"]] = _spec
251
+
252
+ # Structured JSON arguments across the catalog use the ``-json`` flag suffix.
253
+ _JSON_ARG_NAMES: frozenset[str] = frozenset(
254
+ name
255
+ for spec in OP_SPECS
256
+ for name in (*spec["json_args"], *spec["list_json_args"])
257
+ )
258
+
259
+ # Boolean-typed SDK options are registered as store_true flags.
260
+ _BOOLEAN_ARG_NAMES: frozenset[str] = frozenset(
261
+ {"force_build", "abort_on_failure", "notifications_enabled"}
262
+ )
263
+
264
+ # Integer-typed SDK options.
265
+ _INT_ARG_NAMES: frozenset[str] = frozenset({"retry_count"})
266
+
267
+
268
+ def _common_parser(*, paginated: bool) -> _ArgumentParser:
269
+ """Build operation-level common options."""
270
+ parser = _ArgumentParser(add_help=False)
271
+ parser.add_argument("--timeout", type=int, default=None)
272
+ parser.add_argument("--format", choices=("json", "toon", "auto"), default="auto")
273
+ parser.add_argument("--pretty", action="store_true")
274
+ if paginated:
275
+ parser.add_argument("--page-size", type=int, default=None, dest="page_size")
276
+ parser.add_argument("--page-token", default=None, dest="page_token")
277
+ parser.add_argument("--all", action="store_true", dest="all_pages")
278
+ parser.add_argument("--max-pages", type=int, default=None, dest="max_pages")
279
+ return parser
280
+
281
+
282
+ def _kebab(name: str) -> str:
283
+ """Convert snake_case names to CLI kebab-case."""
284
+ return name.replace("_", "-")
285
+
286
+
287
+ def _add_kwarg(
288
+ parser: argparse.ArgumentParser, arg_name: str, *, required: bool
289
+ ) -> None:
290
+ """Add one named operation argument."""
291
+ if arg_name in _JSON_ARG_NAMES:
292
+ flag = "--" + _kebab(arg_name) + "-json"
293
+ elif arg_name in _BOOLEAN_ARG_NAMES:
294
+ flag = "--" + _kebab(arg_name)
295
+ parser.add_argument(flag, action="store_true", default=None, dest=arg_name)
296
+ return
297
+ else:
298
+ flag = "--" + _kebab(arg_name)
299
+ kwargs: dict[str, Any] = {"required": required, "default": None, "dest": arg_name}
300
+ if arg_name in _INT_ARG_NAMES:
301
+ kwargs["type"] = int
302
+ parser.add_argument(flag, **kwargs)
303
+
304
+
305
+ def build_parser() -> argparse.ArgumentParser:
306
+ """Build argparse parser with all 20 Orchestration operations."""
307
+ parser = _ArgumentParser(
308
+ prog="pal-found-orchestration",
309
+ description="Foundry Orchestration CLI - 20 Orchestration API v2 operations",
310
+ )
311
+ subparsers = parser.add_subparsers(dest="resource", help="Resource type")
312
+
313
+ for resource in sorted(OPERATION_BY_RESOURCE):
314
+ res_parser = subparsers.add_parser(_kebab(resource))
315
+ op_sub = res_parser.add_subparsers(dest="operation")
316
+ for operation, spec in sorted(OPERATION_BY_RESOURCE[resource].items()):
317
+ is_paginated = (resource, operation) in PAGINATED_OPS
318
+ op_parser = op_sub.add_parser(
319
+ _kebab(operation), parents=[_common_parser(paginated=is_paginated)]
320
+ )
321
+ for arg_name in spec["positional"]:
322
+ op_parser.add_argument(arg_name)
323
+ for arg_name in spec["required"]:
324
+ _add_kwarg(op_parser, arg_name, required=True)
325
+ for arg_name in spec["optional"]:
326
+ if arg_name in {"page_size", "page_token"} and is_paginated:
327
+ continue
328
+ _add_kwarg(op_parser, arg_name, required=False)
329
+
330
+ return parser
331
+
332
+
333
+ def _spec_for(resource: str, operation: str) -> OperationSpec:
334
+ """Return one catalog operation specification."""
335
+ try:
336
+ return OPERATION_BY_RESOURCE[resource][operation]
337
+ except KeyError as exc:
338
+ raise CLIInputError(f"Unknown operation: {resource}.{operation}") from exc
339
+
340
+
341
+ def _get_client(root_client: Any, client_path: tuple[str, ...]) -> Any:
342
+ """Resolve the exact public nested Orchestration SDK resource."""
343
+ client = root_client.orchestration
344
+ for attribute in client_path:
345
+ client = getattr(client, attribute)
346
+ return client
347
+
348
+
349
+ def _required_text(value: Any, *, field: str) -> str:
350
+ """Validate a non-empty string without echoing its value."""
351
+ if not isinstance(value, str) or not value.strip():
352
+ raise CLIInputError(f"{field} must not be empty")
353
+ return value
354
+
355
+
356
+ def _decode_json(value: str, *, field: str) -> Any:
357
+ try:
358
+ return json.loads(value)
359
+ except json.JSONDecodeError as exc:
360
+ raise CLIInputError(f"{field} must contain valid JSON") from exc
361
+
362
+
363
+ def _parse_json_object(value: str, *, field: str) -> dict[str, Any]:
364
+ """Decode one JSON object."""
365
+ result = _decode_json(value, field=field)
366
+ if not isinstance(result, dict):
367
+ raise CLIInputError(f"{field} must be a JSON object")
368
+ return result
369
+
370
+
371
+ def _parse_json_list(value: str, *, field: str) -> list[Any]:
372
+ """Decode one JSON array."""
373
+ result = _decode_json(value, field=field)
374
+ if not isinstance(result, list):
375
+ raise CLIInputError(f"{field} must be a JSON array")
376
+ return result
377
+
378
+
379
+ def _validate_timeout(value: int) -> int:
380
+ """Validate the effective per-attempt timeout from ADR-002."""
381
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 3600:
382
+ raise CLIInputError("timeout must be between 1 and 3600 seconds")
383
+ return value
384
+
385
+
386
+ def _validate_inputs(spec: OperationSpec, args: argparse.Namespace) -> None:
387
+ """Validate scalars and decode JSON arguments before client creation."""
388
+ for name in (*spec["positional"], *spec["required"]):
389
+ value = getattr(args, name, None)
390
+ if isinstance(value, str):
391
+ _required_text(value, field=name)
392
+ for name in spec["json_args"]:
393
+ value = getattr(args, name, None)
394
+ if value is not None:
395
+ setattr(args, name, _parse_json_object(value, field=name))
396
+ for name in spec["list_json_args"]:
397
+ value = getattr(args, name, None)
398
+ if value is not None:
399
+ setattr(args, name, _parse_json_list(value, field=name))
400
+
401
+
402
+ def _model_to_dict(value: Any) -> Any:
403
+ """Convert SDK models to JSON-compatible values."""
404
+ if value is None:
405
+ return None
406
+ if isinstance(value, list):
407
+ return [_model_to_dict(item) for item in value]
408
+ if isinstance(value, dict):
409
+ return {key: _model_to_dict(item) for key, item in value.items()}
410
+ for method_name in ("to_dict", "model_dump", "dict"):
411
+ method = getattr(value, method_name, None)
412
+ if callable(method):
413
+ result = method()
414
+ if isinstance(result, dict):
415
+ return {key: _model_to_dict(item) for key, item in result.items()}
416
+ return value
417
+
418
+
419
+ def _build_kwargs(
420
+ spec: OperationSpec, args: argparse.Namespace, timeout: int
421
+ ) -> dict[str, Any]:
422
+ """Build SDK keyword arguments, omitting absent optional values."""
423
+ is_paginated = (spec["resource"], spec["operation"]) in PAGINATED_OPS
424
+ kwargs: dict[str, Any] = {}
425
+ for name in (*spec["required"], *spec["optional"]):
426
+ if name in {"page_size", "page_token"} and is_paginated:
427
+ continue
428
+ value = getattr(args, name, None)
429
+ if value is not None:
430
+ kwargs[name] = value
431
+ kwargs["request_timeout"] = timeout
432
+ return kwargs
433
+
434
+
435
+ async def _invoke(
436
+ spec: OperationSpec,
437
+ client: Any,
438
+ args: argparse.Namespace,
439
+ timeout: int,
440
+ ) -> Any:
441
+ """Invoke one non-paginated SDK operation."""
442
+ positional = [getattr(args, name) for name in spec["positional"]]
443
+ kwargs = _build_kwargs(spec, args, timeout)
444
+ if spec["operation"] == "get_batch":
445
+ # The CLI accepts a flat list of RIDs; the SDK body expects one
446
+ # request element per RID.
447
+ rid_field = {
448
+ "build": "build_rid",
449
+ "job": "job_rid",
450
+ "schedule": "schedule_rid",
451
+ }[spec["resource"]]
452
+ rids_key = f"{spec['resource']}_rids"
453
+ rids = kwargs.pop(rids_key)
454
+ kwargs["body"] = [{rid_field: rid} for rid in rids]
455
+ return await getattr(client, spec["method"])(
456
+ *positional, **kwargs
457
+ )
458
+
459
+
460
+ async def _fetch_raw_page(
461
+ method: Any,
462
+ *,
463
+ page_size: int,
464
+ page_token: str | None,
465
+ request_timeout: int | None,
466
+ positional: tuple[Any, ...],
467
+ extra_kwargs: dict[str, Any],
468
+ ) -> dict[str, Any]:
469
+ """Fetch exactly one decoded SDK page via with_raw_response."""
470
+ raw_response = await method(
471
+ *positional,
472
+ page_size=page_size,
473
+ page_token=page_token,
474
+ request_timeout=request_timeout,
475
+ **extra_kwargs,
476
+ )
477
+ page = raw_response.decode()
478
+ return {
479
+ "items": list(getattr(page, "data", None) or []),
480
+ "next_page_token": getattr(page, "next_page_token", None),
481
+ }
482
+
483
+
484
+ def _resolve_pagination_flags(args: argparse.Namespace) -> int:
485
+ """Resolve the effective page batch size from --all/--max-pages."""
486
+ if getattr(args, "all_pages", False):
487
+ return HARD_MAX_BATCH_PAGES
488
+ return getattr(args, "max_pages", None) or 1
489
+
490
+
491
+ async def _paginate_operation(
492
+ spec: OperationSpec,
493
+ client: Any,
494
+ args: argparse.Namespace,
495
+ timeout: int,
496
+ ) -> tuple[list[Any], PaginationHelper]:
497
+ """Collect a bounded number of actual server pages."""
498
+ raw_method = getattr(getattr(client, "with_raw_response"), spec["method"])
499
+ batch_pages = _resolve_pagination_flags(args)
500
+ helper = PaginationHelper(
501
+ page_size=getattr(args, "page_size", None),
502
+ page_token=getattr(args, "page_token", None),
503
+ batch_pages=batch_pages,
504
+ )
505
+ positional = tuple(getattr(args, name) for name in spec["positional"])
506
+ extra_kwargs: dict[str, Any] = {}
507
+ for name in spec["optional"]:
508
+ if name in {"page_size", "page_token"}:
509
+ continue
510
+ value = getattr(args, name, None)
511
+ if value is not None:
512
+ extra_kwargs[name] = value
513
+
514
+ async def fetch_page(**page_kwargs: Any) -> dict[str, Any]:
515
+ return await _fetch_raw_page(
516
+ raw_method,
517
+ page_size=page_kwargs["page_size"],
518
+ page_token=page_kwargs.get("page_token"),
519
+ request_timeout=timeout,
520
+ positional=positional,
521
+ extra_kwargs=extra_kwargs,
522
+ )
523
+
524
+ return await helper.paginate(fetch_page), helper
525
+
526
+
527
+ def _serialize_error(exception: BaseException) -> int:
528
+ """Write one privacy-safe ADR-001 error envelope to stdout."""
529
+ serializer = ErrorSerializer()
530
+ status = sdk_http_status(exception)
531
+ declared = getattr(exception, "exit_code", None)
532
+ sdk_exit = sdk_exception_exit_code(exception)
533
+ if isinstance(declared, int) and 1 <= declared <= 9:
534
+ exit_code = declared
535
+ elif sdk_exit is not None:
536
+ exit_code = sdk_exit
537
+ elif status == 401:
538
+ exit_code = EXIT_AUTH
539
+ elif status == 403:
540
+ exit_code = EXIT_PERMISSION_DENIED
541
+ elif status == 404:
542
+ exit_code = EXIT_NOT_FOUND
543
+ elif status == 429:
544
+ exit_code = EXIT_RATE_LIMIT
545
+ elif isinstance(exception, ConfigurationError):
546
+ exit_code = EXIT_CONFIGURATION
547
+ elif isinstance(exception, (TypeError, ValueError)):
548
+ exit_code = EXIT_USER_INPUT
549
+ elif isinstance(exception, TimeoutError):
550
+ exit_code = EXIT_TIMEOUT
551
+ else:
552
+ exit_code = EXIT_SERVER_ERROR
553
+
554
+ safe = isinstance(
555
+ exception,
556
+ (CLIInputError, ConfigurationError, TimeoutError),
557
+ ) or declared in {EXIT_ACCESS_CONTROL, EXIT_CONFIGURATION}
558
+ message = str(exception) if safe else "Orchestration operation failed"
559
+ envelope = serializer.create_error_envelope(
560
+ exit_code, message, type(exception).__name__, serializer.call_id
561
+ )
562
+ if isinstance(status, int):
563
+ envelope["http_status"] = status
564
+ sys.stdout.write(json.dumps(envelope, default=str) + "\n")
565
+ sys.stdout.flush()
566
+ logger.error(
567
+ "Orchestration operation failed",
568
+ extra={
569
+ "call_id": serializer.call_id,
570
+ "exception_type": type(exception).__name__,
571
+ "http_status": status,
572
+ },
573
+ )
574
+ return exit_code
575
+
576
+
577
+ async def main() -> int:
578
+ """Run one stateless Foundry Orchestration invocation."""
579
+ parser = build_parser()
580
+ try:
581
+ args = parser.parse_args()
582
+ if not args.resource or not getattr(args, "operation", None):
583
+ raise CLIInputError("an Orchestration operation is required")
584
+ except CLIInputError as exc:
585
+ return _serialize_error(exc)
586
+
587
+ try:
588
+ cfg = ConfigLoader()
589
+ cfg.load()
590
+ LogSetup.configure(log_level=cfg.log_level)
591
+
592
+ resource = args.resource.replace("-", "_")
593
+ operation = args.operation.replace("-", "_")
594
+ spec = _spec_for(resource, operation)
595
+ _validate_inputs(spec, args)
596
+ timeout = _validate_timeout(
597
+ args.timeout if args.timeout is not None else cfg.timeout_s
598
+ )
599
+ AccessControlGuard(
600
+ cfg,
601
+ "ORCHESTRATION",
602
+ metadata_allowlist_path=str(_METADATA_ALLOWLIST_PATH),
603
+ ).check(resource, operation)
604
+
605
+ factory = AsyncClientFactory()
606
+ helper: PaginationHelper | None = None
607
+ with factory.invocation_scope(cfg, include_attribution=False):
608
+ root_client = factory.create(cfg, include_attribution=False)
609
+ client = _get_client(root_client, spec["client_path"])
610
+ retry_handler = RetryHandler(timeout_s=timeout)
611
+ if (resource, operation) in PAGINATED_OPS:
612
+ result, helper = await retry_handler.execute(
613
+ _paginate_operation, spec, client, args, timeout
614
+ )
615
+ else:
616
+ result = await retry_handler.execute(
617
+ _invoke, spec, client, args, timeout
618
+ )
619
+
620
+ formatter = OutputFormatter(format_setting=args.format, pretty=args.pretty)
621
+ print(formatter.format(_model_to_dict(result)))
622
+ if helper is not None:
623
+ helper.emit_metadata()
624
+ return EXIT_SUCCESS
625
+ except AccessControlError as exc:
626
+ ErrorSerializer().serialize(exc)
627
+ return EXIT_ACCESS_CONTROL
628
+ except asyncio.CancelledError:
629
+ return _serialize_error(TimeoutError("Operation cancelled"))
630
+ except Exception as exc:
631
+ return _serialize_error(exc)
632
+
633
+
634
+ def console_main() -> int:
635
+ """Run the async CLI through one event-loop boundary."""
636
+ return asyncio.run(main())
637
+
638
+
639
+ if __name__ == "__main__":
640
+ raise SystemExit(console_main())
@@ -0,0 +1,9 @@
1
+ """Foundry SQL Queries CLI package."""
2
+
3
+ from pal_found_cli.sql_queries.scripts.pal_found_sql_queries_cli import (
4
+ build_parser,
5
+ console_main,
6
+ main,
7
+ )
8
+
9
+ __all__ = ["build_parser", "console_main", "main"]
@@ -0,0 +1,9 @@
1
+ # SqlQueries metadata allow-list
2
+
3
+ | SDK Path | Status | Rationale |
4
+ | --- | --- | --- |
5
+ | `sql_queries.sql_query.cancel` | BLOCKED | Cancels a running query |
6
+ | `sql_queries.sql_query.execute` | BLOCKED | Executes billable SQL work |
7
+ | `sql_queries.sql_query.execute_ontology` | BLOCKED | Executes billable SQL against the Ontology |
8
+ | `sql_queries.sql_query.get_results` | BLOCKED | Streams query result content bytes |
9
+ | `sql_queries.sql_query.get_status` | PERMITTED | Query status metadata |