ostruct-cli 0.8.29__py3-none-any.whl → 1.0.1__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 (49) hide show
  1. ostruct/cli/__init__.py +3 -15
  2. ostruct/cli/attachment_processor.py +455 -0
  3. ostruct/cli/attachment_template_bridge.py +973 -0
  4. ostruct/cli/cli.py +157 -33
  5. ostruct/cli/click_options.py +775 -692
  6. ostruct/cli/code_interpreter.py +195 -12
  7. ostruct/cli/commands/__init__.py +0 -3
  8. ostruct/cli/commands/run.py +289 -62
  9. ostruct/cli/config.py +23 -22
  10. ostruct/cli/constants.py +89 -0
  11. ostruct/cli/errors.py +175 -5
  12. ostruct/cli/explicit_file_processor.py +0 -15
  13. ostruct/cli/file_info.py +97 -15
  14. ostruct/cli/file_list.py +43 -1
  15. ostruct/cli/file_search.py +68 -2
  16. ostruct/cli/help_json.py +235 -0
  17. ostruct/cli/mcp_integration.py +13 -16
  18. ostruct/cli/params.py +217 -0
  19. ostruct/cli/plan_assembly.py +335 -0
  20. ostruct/cli/plan_printing.py +385 -0
  21. ostruct/cli/progress_reporting.py +8 -56
  22. ostruct/cli/quick_ref_help.py +128 -0
  23. ostruct/cli/rich_config.py +299 -0
  24. ostruct/cli/runner.py +397 -190
  25. ostruct/cli/security/__init__.py +2 -0
  26. ostruct/cli/security/allowed_checker.py +41 -0
  27. ostruct/cli/security/normalization.py +13 -9
  28. ostruct/cli/security/security_manager.py +558 -17
  29. ostruct/cli/security/types.py +15 -0
  30. ostruct/cli/template_debug.py +283 -261
  31. ostruct/cli/template_debug_help.py +233 -142
  32. ostruct/cli/template_env.py +46 -5
  33. ostruct/cli/template_filters.py +415 -8
  34. ostruct/cli/template_processor.py +240 -619
  35. ostruct/cli/template_rendering.py +49 -73
  36. ostruct/cli/template_validation.py +2 -1
  37. ostruct/cli/token_validation.py +35 -15
  38. ostruct/cli/types.py +15 -19
  39. ostruct/cli/unicode_compat.py +283 -0
  40. ostruct/cli/upload_manager.py +448 -0
  41. ostruct/cli/validators.py +255 -54
  42. {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/METADATA +231 -128
  43. ostruct_cli-1.0.1.dist-info/RECORD +80 -0
  44. ostruct/cli/commands/quick_ref.py +0 -54
  45. ostruct/cli/template_optimizer.py +0 -478
  46. ostruct_cli-0.8.29.dist-info/RECORD +0 -71
  47. {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/LICENSE +0 -0
  48. {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/WHEEL +0 -0
  49. {ostruct_cli-0.8.29.dist-info → ostruct_cli-1.0.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,335 @@
1
+ """Plan assembly for execution plans and run summaries.
2
+
3
+ This module provides the single source of truth for plan data structures
4
+ following UNIFIED GUIDELINES to prevent logic drift between JSON and human output.
5
+ """
6
+
7
+ import os
8
+ import time
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from .attachment_processor import ProcessedAttachments
14
+
15
+
16
+ class PlanAssembler:
17
+ """Single source of truth for execution plan data structure.
18
+
19
+ This class ensures consistent plan format across all output types
20
+ (JSON, human-readable, etc.) by providing a single build method.
21
+ """
22
+
23
+ @staticmethod
24
+ def validate_download_configuration(
25
+ enabled_tools: Optional[set[str]] = None,
26
+ ci_config: Optional[Dict[str, Any]] = None,
27
+ expected_files: Optional[List[str]] = None,
28
+ ) -> Dict[str, Any]:
29
+ """Validate download configuration for dry-run.
30
+
31
+ Args:
32
+ enabled_tools: Set of enabled tools
33
+ ci_config: Code Interpreter configuration
34
+ expected_files: List of expected output filenames (optional)
35
+
36
+ Returns:
37
+ Dictionary with validation results
38
+ """
39
+ validation: Dict[str, Any] = {
40
+ "enabled": False,
41
+ "directory": None,
42
+ "writable": False,
43
+ "conflicts": [],
44
+ "issues": [],
45
+ }
46
+
47
+ # Check if Code Interpreter is enabled
48
+ if not enabled_tools or "code-interpreter" not in enabled_tools:
49
+ return validation
50
+
51
+ validation["enabled"] = True
52
+
53
+ # Get download directory
54
+
55
+ download_dir = "./downloads" # Default
56
+ if ci_config:
57
+ download_dir = ci_config.get("output_directory", download_dir)
58
+
59
+ validation["directory"] = download_dir
60
+
61
+ # Check directory permissions
62
+ try:
63
+ download_path = Path(download_dir)
64
+ parent_dir = download_path.parent
65
+
66
+ # Check if parent exists and is writable
67
+ if parent_dir.exists():
68
+ # Try to create a test file in parent
69
+ test_file = parent_dir / ".ostruct_write_test"
70
+ try:
71
+ test_file.touch()
72
+ test_file.unlink()
73
+
74
+ # If download dir exists, check it specifically
75
+ if download_path.exists():
76
+ if not download_path.is_dir():
77
+ validation["issues"].append(
78
+ f"Path exists but is not a directory: {download_dir}"
79
+ )
80
+ else:
81
+ # Test write in actual directory
82
+ test_file = download_path / ".ostruct_write_test"
83
+ test_file.touch()
84
+ test_file.unlink()
85
+ validation["writable"] = True
86
+ else:
87
+ # Directory doesn't exist but parent is writable
88
+ validation["writable"] = True
89
+
90
+ except Exception as e:
91
+ validation["issues"].append(
92
+ f"Cannot write to directory: {e}"
93
+ )
94
+ else:
95
+ validation["issues"].append(
96
+ f"Parent directory does not exist: {parent_dir}"
97
+ )
98
+
99
+ except Exception as e:
100
+ validation["issues"].append(f"Error checking directory: {e}")
101
+
102
+ # Check for potential conflicts if expected files provided
103
+ if expected_files and validation["writable"]:
104
+ try:
105
+ download_path = Path(download_dir)
106
+ if download_path.exists():
107
+ existing_files = {
108
+ f.name for f in download_path.iterdir() if f.is_file()
109
+ }
110
+ conflicts = [
111
+ f for f in expected_files if f in existing_files
112
+ ]
113
+ validation["conflicts"] = conflicts
114
+ except Exception:
115
+ pass # Ignore errors in conflict detection
116
+
117
+ return validation
118
+
119
+ @staticmethod
120
+ def build_execution_plan(
121
+ processed_attachments: ProcessedAttachments,
122
+ template_path: str,
123
+ schema_path: str,
124
+ variables: Dict[str, Any],
125
+ security_mode: Optional[str] = None,
126
+ model: Optional[str] = None,
127
+ **kwargs: Any,
128
+ ) -> Dict[str, Any]:
129
+ """Build execution plan dict with consistent schema.
130
+
131
+ Args:
132
+ processed_attachments: Processed attachment specifications
133
+ template_path: Path to template file
134
+ schema_path: Path to schema file
135
+ variables: Template variables
136
+ security_mode: Security mode setting
137
+ model: Model to use for processing
138
+ **kwargs: Additional context (allowed_paths, cost_estimate, template_warning, etc.)
139
+
140
+ Returns:
141
+ Execution plan dictionary
142
+ """
143
+ # Handle template warning information
144
+ template_warning = kwargs.get("template_warning")
145
+ original_template_path = kwargs.get("original_template_path")
146
+
147
+ # Use original path if available, otherwise use the provided path
148
+ display_path = original_template_path or template_path
149
+
150
+ template_info = {
151
+ "path": display_path,
152
+ "exists": (
153
+ Path(display_path).exists() if display_path != "---" else True
154
+ ),
155
+ }
156
+
157
+ # Add warning information if present
158
+ if template_warning:
159
+ template_info["warning"] = template_warning
160
+
161
+ schema_info = {
162
+ "path": schema_path,
163
+ "exists": Path(schema_path).exists(),
164
+ }
165
+
166
+ # Build plan structure
167
+ plan = {
168
+ "schema_version": "1.0",
169
+ "type": "execution_plan",
170
+ "timestamp": datetime.now().isoformat(),
171
+ "template": template_info,
172
+ "schema": schema_info,
173
+ "model": model or "gpt-4o",
174
+ "variables": variables,
175
+ "security_mode": security_mode or "permissive",
176
+ "attachments": PlanAssembler._format_attachments(
177
+ processed_attachments
178
+ ),
179
+ }
180
+
181
+ # Add tools section if enabled tools are provided
182
+ enabled_tools = kwargs.get("enabled_tools")
183
+ if enabled_tools:
184
+ tools_dict = {}
185
+ for tool in enabled_tools:
186
+ # Map tool names to boolean values for plan display
187
+ if tool == "code-interpreter":
188
+ tools_dict["code_interpreter"] = True
189
+ elif tool == "file-search":
190
+ tools_dict["file_search"] = True
191
+ elif tool == "web-search":
192
+ tools_dict["web_search"] = True
193
+ elif tool == "mcp":
194
+ tools_dict["mcp"] = True
195
+ elif (
196
+ tool != "template"
197
+ ): # Skip template as it's not an external tool
198
+ tools_dict[tool] = True
199
+
200
+ if tools_dict:
201
+ plan["tools"] = tools_dict
202
+
203
+ # Add download validation for Code Interpreter
204
+ if enabled_tools and "code-interpreter" in enabled_tools:
205
+ # Get CI config if available
206
+ ci_config = kwargs.get("ci_config")
207
+ download_validation = (
208
+ PlanAssembler.validate_download_configuration(
209
+ enabled_tools=enabled_tools,
210
+ ci_config=ci_config,
211
+ expected_files=kwargs.get("expected_files"),
212
+ )
213
+ )
214
+
215
+ # Add to plan if there are issues or useful info
216
+ if download_validation["enabled"]:
217
+ plan["download_validation"] = download_validation
218
+
219
+ # Add optional fields
220
+ if kwargs.get("allowed_paths"):
221
+ plan["allowed_paths"] = kwargs["allowed_paths"]
222
+ if kwargs.get("cost_estimate"):
223
+ plan["cost_estimate"] = kwargs["cost_estimate"]
224
+
225
+ return plan
226
+
227
+ @staticmethod
228
+ def build_run_summary(
229
+ execution_plan: Dict[str, Any],
230
+ result: Optional[Dict[str, Any]] = None,
231
+ execution_time: Optional[float] = None,
232
+ **kwargs: Any,
233
+ ) -> Dict[str, Any]:
234
+ """Build run summary dict from execution plan and results.
235
+
236
+ Args:
237
+ execution_plan: Original execution plan
238
+ result: Execution results
239
+ execution_time: Time taken for execution
240
+ **kwargs: Additional summary data
241
+
242
+ Returns:
243
+ Dictionary with consistent run summary structure
244
+ """
245
+ summary = {
246
+ "schema_version": "1.0",
247
+ "type": "run_summary",
248
+ "timestamp": time.time(),
249
+ "execution_time": execution_time,
250
+ "success": kwargs.get("success", True),
251
+ "original_plan": execution_plan,
252
+ }
253
+
254
+ if result:
255
+ summary["result"] = result
256
+
257
+ if "error" in kwargs:
258
+ summary["error"] = kwargs["error"]
259
+ summary["success"] = False
260
+
261
+ if "cost_breakdown" in kwargs:
262
+ summary["cost_breakdown"] = kwargs["cost_breakdown"]
263
+
264
+ return summary
265
+
266
+ @staticmethod
267
+ def _format_attachments(
268
+ processed_attachments: ProcessedAttachments,
269
+ ) -> List[Dict[str, Any]]:
270
+ """Format processed attachments for plan output.
271
+
272
+ Args:
273
+ processed_attachments: Processed attachment specifications
274
+
275
+ Returns:
276
+ List of formatted attachment dictionaries
277
+ """
278
+ attachments = []
279
+
280
+ # Add all attachment types with consistent format
281
+ for alias, spec in processed_attachments.alias_map.items():
282
+ attachment = {
283
+ "alias": alias,
284
+ "path": spec.path,
285
+ "targets": sorted(
286
+ list(spec.targets)
287
+ ), # Ensure consistent ordering
288
+ "type": "file" if not spec.recursive else "directory",
289
+ "exists": os.path.exists(spec.path),
290
+ "recursive": spec.recursive,
291
+ "pattern": spec.pattern,
292
+ }
293
+
294
+ # Add metadata about where this attachment will be processed
295
+ tool_info = []
296
+ if "prompt" in spec.targets:
297
+ tool_info.append("template")
298
+ if "code-interpreter" in spec.targets or "ci" in spec.targets:
299
+ tool_info.append("code_interpreter")
300
+ if "file-search" in spec.targets or "fs" in spec.targets:
301
+ tool_info.append("file_search")
302
+
303
+ attachment["processing"] = tool_info
304
+ attachments.append(attachment)
305
+
306
+ return attachments
307
+
308
+ @staticmethod
309
+ def validate_plan(plan: Dict[str, Any]) -> bool:
310
+ """Validate plan structure for consistency.
311
+
312
+ Args:
313
+ plan: Plan dictionary to validate
314
+
315
+ Returns:
316
+ True if plan structure is valid
317
+
318
+ Raises:
319
+ ValueError: If plan structure is invalid
320
+ """
321
+ required_fields = ["schema_version", "type", "timestamp"]
322
+
323
+ for field in required_fields:
324
+ if field not in plan:
325
+ raise ValueError(f"Missing required field: {field}")
326
+
327
+ if plan["type"] not in ["execution_plan", "run_summary"]:
328
+ raise ValueError(f"Invalid plan type: {plan['type']}")
329
+
330
+ if plan["schema_version"] != "1.0":
331
+ raise ValueError(
332
+ f"Unsupported schema version: {plan['schema_version']}"
333
+ )
334
+
335
+ return True