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.
- pal_found_cli/__init__.py +1 -0
- pal_found_cli/_version.py +24 -0
- pal_found_cli/admin/__init__.py +2 -0
- pal_found_cli/admin/scripts/__init__.py +2 -0
- pal_found_cli/admin/scripts/pal_found_admin_cli.py +600 -0
- pal_found_cli/aip_agents/__init__.py +1 -0
- pal_found_cli/aip_agents/metadata-allow-list.md +20 -0
- pal_found_cli/aip_agents/scripts/__init__.py +1 -0
- pal_found_cli/aip_agents/scripts/pal_found_aip_agents_cli.py +704 -0
- pal_found_cli/audit/__init__.py +1 -0
- pal_found_cli/audit/metadata-allow-list.md +4 -0
- pal_found_cli/audit/scripts/__init__.py +1 -0
- pal_found_cli/audit/scripts/pal_found_audit_cli.py +387 -0
- pal_found_cli/checkpoints/__init__.py +9 -0
- pal_found_cli/checkpoints/metadata-allow-list.md +7 -0
- pal_found_cli/checkpoints/scripts/__init__.py +1 -0
- pal_found_cli/checkpoints/scripts/pal_found_checkpoints_cli.py +527 -0
- pal_found_cli/common/__init__.py +56 -0
- pal_found_cli/common/access_control_guard.py +591 -0
- pal_found_cli/common/async_client_factory.py +161 -0
- pal_found_cli/common/auth_provider.py +82 -0
- pal_found_cli/common/binary_download_handler.py +288 -0
- pal_found_cli/common/config_loader.py +388 -0
- pal_found_cli/common/error_serializer.py +412 -0
- pal_found_cli/common/log_setup.py +241 -0
- pal_found_cli/common/metadata-allow-list.md +169 -0
- pal_found_cli/common/output_formatter.py +267 -0
- pal_found_cli/common/pagination_helper.py +212 -0
- pal_found_cli/common/retry.py +617 -0
- pal_found_cli/common/sdk_error_utils.py +112 -0
- pal_found_cli/common/session_manager.py +461 -0
- pal_found_cli/common/tracing_provider.py +107 -0
- pal_found_cli/connectivity/__init__.py +9 -0
- pal_found_cli/connectivity/metadata-allow-list.md +24 -0
- pal_found_cli/connectivity/scripts/__init__.py +1 -0
- pal_found_cli/connectivity/scripts/pal_found_connectivity_cli.py +720 -0
- pal_found_cli/data_health/__init__.py +9 -0
- pal_found_cli/data_health/metadata-allow-list.md +10 -0
- pal_found_cli/data_health/scripts/__init__.py +1 -0
- pal_found_cli/data_health/scripts/pal_found_data_health_cli.py +458 -0
- pal_found_cli/datasets/__init__.py +1 -0
- pal_found_cli/datasets/scripts/__init__.py +1 -0
- pal_found_cli/datasets/scripts/pal_found_datasets_cli.py +732 -0
- pal_found_cli/filesystem/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/pal_found_filesystem_cli.py +659 -0
- pal_found_cli/functions/__init__.py +2 -0
- pal_found_cli/functions/scripts/__init__.py +2 -0
- pal_found_cli/functions/scripts/pal_found_functions_cli.py +389 -0
- pal_found_cli/language_models/__init__.py +1 -0
- pal_found_cli/language_models/metadata-allow-list.md +6 -0
- pal_found_cli/language_models/scripts/__init__.py +1 -0
- pal_found_cli/language_models/scripts/pal_found_language_models_cli.py +342 -0
- pal_found_cli/media_sets/__init__.py +9 -0
- pal_found_cli/media_sets/metadata-allow-list.md +23 -0
- pal_found_cli/media_sets/scripts/__init__.py +1 -0
- pal_found_cli/media_sets/scripts/pal_found_media_sets_cli.py +642 -0
- pal_found_cli/models/__init__.py +5 -0
- pal_found_cli/models/metadata-allow-list.md +27 -0
- pal_found_cli/models/scripts/__init__.py +1 -0
- pal_found_cli/models/scripts/pal_found_models_cli.py +714 -0
- pal_found_cli/ontologies/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/pal_found_ontologies_cli.py +1225 -0
- pal_found_cli/orchestration/__init__.py +9 -0
- pal_found_cli/orchestration/metadata-allow-list.md +24 -0
- pal_found_cli/orchestration/scripts/__init__.py +1 -0
- pal_found_cli/orchestration/scripts/pal_found_orchestration_cli.py +640 -0
- pal_found_cli/sql_queries/__init__.py +9 -0
- pal_found_cli/sql_queries/metadata-allow-list.md +9 -0
- pal_found_cli/sql_queries/scripts/__init__.py +1 -0
- pal_found_cli/sql_queries/scripts/pal_found_sql_queries_cli.py +514 -0
- pal_found_cli/streams/__init__.py +9 -0
- pal_found_cli/streams/metadata-allow-list.md +19 -0
- pal_found_cli/streams/scripts/__init__.py +1 -0
- pal_found_cli/streams/scripts/pal_found_streams_cli.py +615 -0
- pal_found_cli/third_party_applications/__init__.py +9 -0
- pal_found_cli/third_party_applications/metadata-allow-list.md +13 -0
- pal_found_cli/third_party_applications/scripts/__init__.py +1 -0
- pal_found_cli/third_party_applications/scripts/pal_found_third_party_applications_cli.py +547 -0
- pal_found_cli/widgets/__init__.py +9 -0
- pal_found_cli/widgets/metadata-allow-list.md +12 -0
- pal_found_cli/widgets/scripts/__init__.py +1 -0
- pal_found_cli/widgets/scripts/pal_found_widgets_cli.py +571 -0
- pal_found_cli-0.1.2.dist-info/METADATA +74 -0
- pal_found_cli-0.1.2.dist-info/RECORD +89 -0
- pal_found_cli-0.1.2.dist-info/WHEEL +5 -0
- pal_found_cli-0.1.2.dist-info/entry_points.txt +19 -0
- 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,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."""
|