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,169 @@
1
+ # Core metadata-only allow-list
2
+
3
+ This packaged policy covers namespaces that share `AccessControlGuard`'s
4
+ default allow-list. Namespace packages with their own policy file pass that
5
+ path explicitly.
6
+
7
+ | SDK operation | Decision | Notes |
8
+ | --- | --- | --- |
9
+ | `admin.authentication_provider.get` | PERMITTED | |
10
+ | `admin.authentication_provider.list` | PERMITTED | |
11
+ | `admin.cbac_banner.get` | PERMITTED | |
12
+ | `admin.cbac_marking_restrictions.get` | PERMITTED | |
13
+ | `admin.enrollment.get` | PERMITTED | |
14
+ | `admin.enrollment.get_current` | PERMITTED | |
15
+ | `admin.enrollment_role_assignment.list` | PERMITTED | |
16
+ | `admin.group.get` | PERMITTED | |
17
+ | `admin.group.get_batch` | PERMITTED | |
18
+ | `admin.group.list` | PERMITTED | |
19
+ | `admin.group.list_current` | PERMITTED | |
20
+ | `admin.group.search` | PERMITTED | |
21
+ | `admin.group_member.list` | PERMITTED | |
22
+ | `admin.group_membership.list` | PERMITTED | |
23
+ | `admin.group_membership_expiration_policy.get` | PERMITTED | |
24
+ | `admin.group_provider_info.get` | PERMITTED | |
25
+ | `admin.host.list` | PERMITTED | |
26
+ | `admin.marking.get` | PERMITTED | |
27
+ | `admin.marking.get_batch` | PERMITTED | |
28
+ | `admin.marking.list` | PERMITTED | |
29
+ | `admin.marking_category.get` | PERMITTED | |
30
+ | `admin.marking_category.list` | PERMITTED | |
31
+ | `admin.marking_member.list` | PERMITTED | |
32
+ | `admin.marking_role_assignment.list` | PERMITTED | |
33
+ | `admin.organization.get` | PERMITTED | |
34
+ | `admin.organization.list_available_roles` | PERMITTED | |
35
+ | `admin.organization_guest_member.list` | PERMITTED | |
36
+ | `admin.organization_role_assignment.list` | PERMITTED | |
37
+ | `admin.role.get` | PERMITTED | |
38
+ | `admin.role.get_batch` | PERMITTED | |
39
+ | `admin.user.get` | PERMITTED | |
40
+ | `admin.user.get_batch` | PERMITTED | |
41
+ | `admin.user.get_current` | PERMITTED | |
42
+ | `admin.user.get_markings` | PERMITTED | |
43
+ | `admin.user.list` | PERMITTED | |
44
+ | `admin.user.search` | PERMITTED | |
45
+ | `admin.user_provider_info.get` | PERMITTED | |
46
+ | `datasets.branch.get` | PERMITTED | |
47
+ | `datasets.branch.list` | PERMITTED | |
48
+ | `datasets.branch.transactions` | PERMITTED | |
49
+ | `datasets.dataset.get` | PERMITTED | |
50
+ | `datasets.dataset.get_health_check_reports` | PERMITTED | |
51
+ | `datasets.dataset.get_health_checks` | PERMITTED | |
52
+ | `datasets.dataset.get_schedules` | PERMITTED | |
53
+ | `datasets.dataset.get_schema` | PERMITTED | |
54
+ | `datasets.dataset.get_schema_batch` | PERMITTED | |
55
+ | `datasets.dataset.jobs` | PERMITTED | |
56
+ | `datasets.dataset.transactions` | PERMITTED | |
57
+ | `datasets.file.get` | PERMITTED | |
58
+ | `datasets.file.list` | PERMITTED | |
59
+ | `datasets.transaction.get` | PERMITTED | |
60
+ | `datasets.transaction.job` | PERMITTED | |
61
+ | `datasets.view.get` | PERMITTED | |
62
+ | `filesystem.folder.children` | PERMITTED | |
63
+ | `filesystem.folder.get` | PERMITTED | |
64
+ | `filesystem.folder.get_batch` | PERMITTED | |
65
+ | `filesystem.project.get` | PERMITTED | |
66
+ | `filesystem.project.organizations` | PERMITTED | |
67
+ | `filesystem.resource.get` | PERMITTED | |
68
+ | `filesystem.resource.get_access_requirements` | PERMITTED | |
69
+ | `filesystem.resource.get_batch` | PERMITTED | |
70
+ | `filesystem.resource.get_by_path` | PERMITTED | |
71
+ | `filesystem.resource.get_by_path_batch` | PERMITTED | |
72
+ | `filesystem.resource.markings` | PERMITTED | |
73
+ | `filesystem.resource_role.list` | PERMITTED | |
74
+ | `filesystem.space.get` | PERMITTED | |
75
+ | `filesystem.space.list` | PERMITTED | |
76
+ | `functions.query.get` | PERMITTED | |
77
+ | `functions.query.get_by_rid` | PERMITTED | |
78
+ | `functions.query.get_by_rid_batch` | PERMITTED | |
79
+ | `functions.value_type.get` | PERMITTED | |
80
+ | `functions.version_id.get` | PERMITTED | |
81
+ | `ontologies.action_type.get` | PERMITTED | |
82
+ | `ontologies.action_type.get_by_rid` | PERMITTED | |
83
+ | `ontologies.action_type.get_by_rid_batch` | PERMITTED | |
84
+ | `ontologies.action_type.list` | PERMITTED | |
85
+ | `ontologies.action_type_full_metadata.get` | PERMITTED | |
86
+ | `ontologies.action_type_full_metadata.list` | PERMITTED | |
87
+ | `ontologies.attachment.get` | PERMITTED | |
88
+ | `ontologies.attachment_property.get_attachment` | PERMITTED | |
89
+ | `ontologies.attachment_property.get_attachment_by_rid` | PERMITTED | |
90
+ | `ontologies.media_reference_property.get_media_metadata` | PERMITTED | |
91
+ | `ontologies.object_type.get` | PERMITTED | |
92
+ | `ontologies.object_type.get_by_rid_batch` | PERMITTED | |
93
+ | `ontologies.object_type.get_full_metadata` | PERMITTED | |
94
+ | `ontologies.object_type.get_outgoing_link_type` | PERMITTED | |
95
+ | `ontologies.object_type.list` | PERMITTED | |
96
+ | `ontologies.object_type.list_outgoing_link_types` | PERMITTED | |
97
+ | `ontologies.ontology.get` | PERMITTED | |
98
+ | `ontologies.ontology.get_full_metadata` | PERMITTED | |
99
+ | `ontologies.ontology.list` | PERMITTED | |
100
+ | `ontologies.ontology.load_metadata` | PERMITTED | |
101
+ | `ontologies.ontology_interface.get` | PERMITTED | |
102
+ | `ontologies.ontology_interface.get_outgoing_interface_link_type` | PERMITTED | |
103
+ | `ontologies.ontology_interface.list` | PERMITTED | |
104
+ | `ontologies.ontology_interface.list_outgoing_interface_link_types` | PERMITTED | |
105
+ | `ontologies.ontology_value_type.get` | PERMITTED | |
106
+ | `ontologies.ontology_value_type.list` | PERMITTED | |
107
+ | `ontologies.query_type.get` | PERMITTED | |
108
+ | `ontologies.query_type.list` | PERMITTED | |
109
+ | `aip_agents.agent.get` | PERMITTED | |
110
+ | `aip_agents.agent_version.get` | PERMITTED | |
111
+ | `aip_agents.agent_version.list` | PERMITTED | |
112
+ | `aip_agents.session.get` | PERMITTED | |
113
+ | `aip_agents.session.list` | PERMITTED | |
114
+ | `aip_agents.session_trace.get` | PERMITTED | |
115
+ | `audit.log_file.list` | PERMITTED | |
116
+ | `checkpoints.record.get` | PERMITTED | |
117
+ | `checkpoints.record.get_batch` | PERMITTED | |
118
+ | `checkpoints.record.search` | PERMITTED | |
119
+ | `connectivity.connection.get` | PERMITTED | |
120
+ | `connectivity.connection.get_configuration` | PERMITTED | |
121
+ | `connectivity.connection.get_configuration_batch` | PERMITTED | |
122
+ | `connectivity.file_import.get` | PERMITTED | |
123
+ | `connectivity.file_import.list` | PERMITTED | |
124
+ | `connectivity.table_import.get` | PERMITTED | |
125
+ | `connectivity.table_import.list` | PERMITTED | |
126
+ | `data_health.check.get` | PERMITTED | |
127
+ | `data_health.check_report.get` | PERMITTED | |
128
+ | `data_health.check_report.get_latest` | PERMITTED | |
129
+ | `media_sets.media_set.get` | PERMITTED | |
130
+ | `media_sets.media_set.get_rid_by_path` | PERMITTED | |
131
+ | `media_sets.media_set.get_status` | PERMITTED | |
132
+ | `media_sets.media_set.info` | PERMITTED | |
133
+ | `media_sets.media_set.metadata` | PERMITTED | |
134
+ | `models.experiment.get` | PERMITTED | |
135
+ | `models.experiment.search` | PERMITTED | |
136
+ | `models.model.get` | PERMITTED | |
137
+ | `models.model_studio.get` | PERMITTED | |
138
+ | `models.model_studio_config_version.get` | PERMITTED | |
139
+ | `models.model_studio_config_version.latest` | PERMITTED | |
140
+ | `models.model_studio_config_version.list` | PERMITTED | |
141
+ | `models.model_studio_run.list` | PERMITTED | |
142
+ | `models.model_studio_trainer.get` | PERMITTED | |
143
+ | `models.model_studio_trainer.list` | PERMITTED | |
144
+ | `models.model_version.get` | PERMITTED | |
145
+ | `models.model_version.list` | PERMITTED | |
146
+ | `orchestration.build.get` | PERMITTED | |
147
+ | `orchestration.build.get_batch` | PERMITTED | |
148
+ | `orchestration.build.jobs` | PERMITTED | |
149
+ | `orchestration.build.search` | PERMITTED | |
150
+ | `orchestration.job.get` | PERMITTED | |
151
+ | `orchestration.job.get_batch` | PERMITTED | |
152
+ | `orchestration.schedule.get` | PERMITTED | |
153
+ | `orchestration.schedule.get_affected_resources` | PERMITTED | |
154
+ | `orchestration.schedule.get_batch` | PERMITTED | |
155
+ | `orchestration.schedule.runs` | PERMITTED | |
156
+ | `orchestration.schedule_version.get` | PERMITTED | |
157
+ | `orchestration.schedule_version.schedule` | PERMITTED | |
158
+ | `sql_queries.sql_query.get_status` | PERMITTED | |
159
+ | `streams.stream.get` | PERMITTED | |
160
+ | `streams.stream.get_end_offsets` | PERMITTED | |
161
+ | `streams.subscriber.get_read_position` | PERMITTED | |
162
+ | `third_party_applications.third_party_application.get` | PERMITTED | |
163
+ | `third_party_applications.version.get` | PERMITTED | |
164
+ | `third_party_applications.version.list` | PERMITTED | |
165
+ | `third_party_applications.website.get` | PERMITTED | |
166
+ | `widgets.release.get` | PERMITTED | |
167
+ | `widgets.release.list` | PERMITTED | |
168
+ | `widgets.repository.get` | PERMITTED | |
169
+ | `widgets.widget_set.get` | PERMITTED | |
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env python3
2
+ """OutputFormatter with auto-selection (ADR-004).
3
+
4
+ Implements the format auto-selection algorithm defined in ADR-004.
5
+ Supports JSON and TOON output formats with intelligent format selection
6
+ based on data shape and user configuration.
7
+
8
+ Format Selection Algorithm (ADR-004)
9
+ ------------------------------------
10
+ 1. Explicit format always wins
11
+ 2. Errors always use JSON
12
+ 3. Non-list top-level always uses JSON
13
+ 4. Empty list uses JSON
14
+ 5. Extract field sets from all items
15
+ 6. Any non-dict item → JSON
16
+ 7. All items must share identical field set → TOON, else JSON
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import sys
22
+ from typing import Any
23
+
24
+ # Environment variable for default format
25
+ ENV_DEFAULT_FORMAT = "FOUNDRY_AGENTIC_CLI_DEFAULT_FORMAT"
26
+
27
+
28
+ class OutputFormatter:
29
+ """Formats output data as JSON or TOON with auto-selection (ADR-004).
30
+
31
+ Implements the 7-step auto-selection algorithm from ADR-004.
32
+ Supports explicit format override and pretty-printing.
33
+
34
+ Parameters
35
+ ----------
36
+ format_setting : str, optional
37
+ Format setting: 'json', 'toon', or 'auto'. Defaults to env var
38
+ `FOUNDRY_AGENTIC_CLI_DEFAULT_FORMAT`, then to 'auto'.
39
+ pretty : bool, optional
40
+ Enable pretty-printing for JSON output. Default: False.
41
+
42
+ Examples
43
+ --------
44
+ >>> formatter = OutputFormatter(format_setting="auto")
45
+ >>> output = formatter.format(data)
46
+ >>> print(output)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ format_setting: str | None = None,
52
+ pretty: bool = False,
53
+ ) -> None:
54
+ self.format_setting = format_setting or os.environ.get(
55
+ ENV_DEFAULT_FORMAT, "auto"
56
+ )
57
+ self.pretty = pretty
58
+
59
+ def _select_format(self, data: Any) -> str:
60
+ """Apply ADR-004 format auto-selection algorithm.
61
+
62
+ Algorithm:
63
+ 1. Explicit format always wins
64
+ 2. Errors always use JSON
65
+ 3. Non-list top-level always uses JSON
66
+ 4. Empty list uses JSON
67
+ 5. Extract field sets from all items
68
+ 6. Any non-dict item → JSON
69
+ 7. All items must share identical field set → TOON, else JSON
70
+
71
+ Parameters
72
+ ----------
73
+ data : Any
74
+ The data to format.
75
+
76
+ Returns
77
+ -------
78
+ str
79
+ Selected format: 'json' or 'toon'.
80
+ """
81
+ # 1. Explicit format always wins
82
+ if self.format_setting in ("json", "toon"):
83
+ return self.format_setting
84
+
85
+ # 2. Errors always use JSON
86
+ if isinstance(data, dict) and data.get("error"):
87
+ return "json"
88
+
89
+ # 3. Non-list top-level always uses JSON
90
+ if not isinstance(data, list):
91
+ return "json"
92
+
93
+ # 4. Empty list uses JSON
94
+ if len(data) == 0:
95
+ return "json"
96
+
97
+ # 5. Extract field sets from all items
98
+ field_sets = []
99
+ for item in data:
100
+ if isinstance(item, dict):
101
+ field_sets.append(frozenset(item.keys()))
102
+
103
+ # 6. Any non-dict item → JSON
104
+ if len(field_sets) != len(data):
105
+ return "json"
106
+
107
+ # 7. All items must share identical field set → TOON, else JSON
108
+ if len(set(field_sets)) == 1:
109
+ return "toon"
110
+
111
+ return "json"
112
+
113
+ def _format_json(self, data: Any) -> str:
114
+ """Format data as JSON.
115
+
116
+ Parameters
117
+ ----------
118
+ data : Any
119
+ Data to serialize as JSON.
120
+
121
+ Returns
122
+ -------
123
+ str
124
+ JSON string.
125
+ """
126
+ if self.pretty:
127
+ return json.dumps(data, indent=2, default=str)
128
+ return json.dumps(data, default=str)
129
+
130
+ def _format_toon(self, data: list[dict[str, Any]]) -> str:
131
+ """Format data as TOON (tabular output).
132
+
133
+ TOON format: human-readable table/line format.
134
+ Each dict item becomes a row with aligned columns.
135
+
136
+ Parameters
137
+ ----------
138
+ data : List[Dict[str, Any]]
139
+ List of uniform dicts to format as table.
140
+
141
+ Returns
142
+ -------
143
+ str
144
+ TOON-formatted string.
145
+ """
146
+ if not data:
147
+ return ""
148
+
149
+ # Extract headers (keys from first item)
150
+ headers = list(data[0].keys())
151
+
152
+ # Calculate column widths
153
+ col_widths = {h: len(str(h)) for h in headers}
154
+ for row in data:
155
+ for h in headers:
156
+ val_len = len(str(row.get(h, "")))
157
+ col_widths[h] = max(col_widths[h], val_len)
158
+
159
+ # Build table
160
+ lines = []
161
+
162
+ # Header line
163
+ header_line = " ".join(str(h).ljust(col_widths[h]) for h in headers)
164
+ lines.append(header_line)
165
+
166
+ # Separator
167
+ separator = " ".join("-" * col_widths[h] for h in headers)
168
+ lines.append(separator)
169
+
170
+ # Data rows
171
+ for row in data:
172
+ row_line = " ".join(
173
+ str(row.get(h, "")).ljust(col_widths[h]) for h in headers
174
+ )
175
+ lines.append(row_line)
176
+
177
+ return "\n".join(lines)
178
+
179
+ def format(self, data: Any) -> str:
180
+ """Format data according to format setting and ADR-004 algorithm.
181
+
182
+ Parameters
183
+ ----------
184
+ data : Any
185
+ Data to format. Can be dict, list, or scalar.
186
+
187
+ Returns
188
+ -------
189
+ str
190
+ Formatted string output.
191
+
192
+ Raises
193
+ ------
194
+ ValueError
195
+ If format_setting is not one of 'json', 'toon', 'auto'.
196
+ """
197
+ if self.format_setting not in ("json", "toon", "auto"):
198
+ raise ValueError(
199
+ f"Invalid format_setting '{self.format_setting}'. "
200
+ f"Must be one of: 'json', 'toon', 'auto'"
201
+ )
202
+
203
+ selected_format = self._select_format(data)
204
+
205
+ if selected_format == "toon":
206
+ if isinstance(data, list) and data and isinstance(data[0], dict):
207
+ return self._format_toon(data)
208
+ # Fallback to JSON if TOON not applicable
209
+ return self._format_json(data)
210
+
211
+ return self._format_json(data)
212
+
213
+ def format_error(self, error_data: dict[str, Any]) -> str:
214
+ """Format error data (always JSON per ADR-004).
215
+
216
+ Parameters
217
+ ----------
218
+ error_data : Dict[str, Any]
219
+ Error envelope dictionary.
220
+
221
+ Returns
222
+ -------
223
+ str
224
+ JSON-formatted error string.
225
+ """
226
+ return self._format_json(error_data)
227
+
228
+ def emit(self, data: Any) -> None:
229
+ """Format and emit data to stdout.
230
+
231
+ Parameters
232
+ ----------
233
+ data : Any
234
+ Data to format and emit.
235
+ """
236
+ output = self.format(data)
237
+ sys.stdout.write(output + "\n")
238
+ sys.stdout.flush()
239
+
240
+ def emit_error(self, error_data: dict[str, Any]) -> None:
241
+ """Format and emit error data to stderr.
242
+
243
+ Per ADR-004, error output always goes to stderr (not stdout),
244
+ keeping the CLI contract of stdout for primary result data only.
245
+
246
+ Parameters
247
+ ----------
248
+ error_data : Dict[str, Any]
249
+ Error envelope dictionary.
250
+ """
251
+ output = self.format_error(error_data)
252
+ sys.stderr.write(output + "\n")
253
+ sys.stderr.flush()
254
+
255
+ def emit_to_stderr(self, data: Any) -> None:
256
+ """Emit data (e.g. pagination metadata) to stderr as JSON.
257
+
258
+ Per ADR-004, pagination metadata always goes to stderr as JSON.
259
+
260
+ Parameters
261
+ ----------
262
+ data : Any
263
+ Data to emit to stderr.
264
+ """
265
+ output = json.dumps(data, default=str)
266
+ sys.stderr.write(output + "\n")
267
+ sys.stderr.flush()
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """PaginationHelper — manages --page-size, --page-token, --batch-pages (SRS §3.5).
3
+
4
+ Handles paginated SDK responses and emits metadata to stderr
5
+ with pagination cursors per ADR-005.
6
+
7
+ Default: return first page only with page_token on stderr.
8
+ Batch mode: agent specifies number of pages (max 40).
9
+
10
+ Environment variables:
11
+ - FOUNDRY_AGENTIC_CLI_DEFAULT_PAGE_SIZE (default: 100)
12
+ - FOUNDRY_AGENTIC_CLI_MAX_BATCH_PAGES (default: 40)
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+ from typing import Any
19
+
20
+ from pal_found_cli.common.log_setup import METADATA_SEPARATOR
21
+
22
+ # Default values (from SRS-001 §5)
23
+ DEFAULT_PAGE_SIZE = int(os.environ.get("FOUNDRY_AGENTIC_CLI_DEFAULT_PAGE_SIZE", "100"))
24
+ HARD_MAX_BATCH_PAGES = 40
25
+ MAX_BATCH_PAGES = min(
26
+ int(
27
+ os.environ.get("FOUNDRY_AGENTIC_CLI_MAX_BATCH_PAGES", str(HARD_MAX_BATCH_PAGES))
28
+ ),
29
+ HARD_MAX_BATCH_PAGES,
30
+ )
31
+
32
+
33
+ class PaginationHelper:
34
+ """Manages pagination for SDK calls.
35
+
36
+ Parameters
37
+ ----------
38
+ page_size : int or None
39
+ Number of items per page. Defaults to FOUNDRY_AGENTIC_CLI_DEFAULT_PAGE_SIZE (100).
40
+ page_token : str or None
41
+ Resume from this cursor.
42
+ batch_pages : int or None
43
+ Number of pages to retrieve (default 1, max FOUNDRY_AGENTIC_CLI_MAX_BATCH_PAGES).
44
+
45
+ Usage
46
+ -----
47
+ >>> helper = PaginationHelper(page_size=50, page_token=None, batch_pages=1)
48
+ >>> result = await helper.paginate(client_method, helper)
49
+ >>> helper.emit_metadata() # writes to stderr
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ page_size: int | None = None,
55
+ page_token: str | None = None,
56
+ batch_pages: int | None = None,
57
+ ) -> None:
58
+ self.page_size = self._validate_positive_int(
59
+ "page_size",
60
+ page_size if page_size is not None else DEFAULT_PAGE_SIZE,
61
+ )
62
+ self.page_token = page_token
63
+ raw_batch = self._validate_positive_int(
64
+ "batch_pages",
65
+ batch_pages if batch_pages is not None else 1,
66
+ )
67
+ self.batch_pages = min(raw_batch, MAX_BATCH_PAGES)
68
+ self._next_page_token: str | None = None
69
+ self._total_items: int = 0
70
+ self._pages_fetched: int = 0
71
+
72
+ @staticmethod
73
+ def _validate_positive_int(name: str, value: int) -> int:
74
+ """Validate that a pagination argument is a positive integer."""
75
+ if isinstance(value, bool) or not isinstance(value, int):
76
+ raise TypeError(f"{name} must be a positive integer")
77
+ if value <= 0:
78
+ raise ValueError(f"{name} must be a positive integer")
79
+ return value
80
+
81
+ @property
82
+ def next_page_token(self) -> str | None:
83
+ """Cursor for next page."""
84
+ return self._next_page_token
85
+
86
+ @property
87
+ def total_items(self) -> int:
88
+ """Total items fetched across all pages."""
89
+ return self._total_items
90
+
91
+ @property
92
+ def pages_fetched(self) -> int:
93
+ """Number of pages fetched."""
94
+ return self._pages_fetched
95
+
96
+ def get_sdk_params(self) -> dict[str, Any]:
97
+ """Get pagination parameters for SDK call.
98
+
99
+ Returns
100
+ -------
101
+ dict
102
+ SDK-compatible pagination kwargs.
103
+ """
104
+ params: dict[str, Any] = {}
105
+ if self.page_size is not None:
106
+ params["page_size"] = self.page_size
107
+ if self.page_token is not None:
108
+ params["page_token"] = self.page_token
109
+ return params
110
+
111
+ def _extract_items(self, response: Any) -> list[Any]:
112
+ """Extract items list from SDK response.
113
+
114
+ Parameters
115
+ ----------
116
+ response : Any
117
+ SDK response object.
118
+
119
+ Returns
120
+ -------
121
+ list
122
+ List of items from the response.
123
+ """
124
+ if isinstance(response, dict):
125
+ return response.get("items", []) or []
126
+ if isinstance(response, list):
127
+ return response
128
+ if hasattr(response, "items"):
129
+ return getattr(response, "items", []) or []
130
+ return [response] if response else []
131
+
132
+ def _extract_next_token(self, response: Any) -> str | None:
133
+ """Extract next page token from SDK response.
134
+
135
+ Parameters
136
+ ----------
137
+ response : Any
138
+ SDK response object.
139
+
140
+ Returns
141
+ -------
142
+ str or None
143
+ Next page token.
144
+ """
145
+ if isinstance(response, dict):
146
+ return response.get("next_page_token") or response.get("nextPageToken")
147
+ if hasattr(response, "next_page_token"):
148
+ return getattr(response, "next_page_token", None)
149
+ if hasattr(response, "nextPageToken"):
150
+ return getattr(response, "nextPageToken", None)
151
+ return None
152
+
153
+ async def paginate(
154
+ self,
155
+ call_func,
156
+ **call_kwargs,
157
+ ) -> list[Any]:
158
+ """Execute paginated SDK calls.
159
+
160
+ Parameters
161
+ ----------
162
+ call_func : callable
163
+ Async SDK method to call.
164
+ **call_kwargs
165
+ Additional kwargs to pass to SDK method.
166
+
167
+ Returns
168
+ -------
169
+ list
170
+ Aggregated items from all pages.
171
+ """
172
+ all_items: list[Any] = []
173
+ token = self.page_token
174
+
175
+ for _ in range(self.batch_pages):
176
+ # Build params
177
+ params = dict(call_kwargs)
178
+ if self.page_size is not None:
179
+ params["page_size"] = self.page_size
180
+ if token is not None:
181
+ params["page_token"] = token
182
+
183
+ response = await call_func(**params)
184
+ items = self._extract_items(response)
185
+ all_items.extend(items)
186
+ self._total_items += len(items)
187
+ self._pages_fetched += 1
188
+
189
+ token = self._extract_next_token(response)
190
+ if token is None:
191
+ break
192
+
193
+ self._next_page_token = token
194
+ return all_items
195
+
196
+ def emit_metadata(self) -> None:
197
+ """Emit pagination metadata to stderr (ADR-005).
198
+
199
+ Writes METADATA_SEPARATOR followed by JSON metadata.
200
+ """
201
+ metadata: dict[str, Any] = {
202
+ "pages_fetched": self._pages_fetched,
203
+ "total_items": self._total_items,
204
+ }
205
+ if self._next_page_token:
206
+ metadata["next_page_token"] = self._next_page_token
207
+ if self.page_size:
208
+ metadata["page_size"] = self.page_size
209
+
210
+ sys.stderr.write(METADATA_SEPARATOR + "\n")
211
+ sys.stderr.write(json.dumps(metadata) + "\n")
212
+ sys.stderr.flush()