ostruct-cli 0.4.0__tar.gz → 0.5.0__tar.gz

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 (44) hide show
  1. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/PKG-INFO +60 -21
  2. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/README.md +56 -16
  3. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/pyproject.toml +25 -19
  4. ostruct_cli-0.5.0/src/ostruct/cli/base_errors.py +183 -0
  5. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/cli.py +822 -543
  6. ostruct_cli-0.5.0/src/ostruct/cli/click_options.py +375 -0
  7. ostruct_cli-0.5.0/src/ostruct/cli/errors.py +474 -0
  8. ostruct_cli-0.5.0/src/ostruct/cli/exit_codes.py +18 -0
  9. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/file_info.py +30 -14
  10. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/file_list.py +4 -10
  11. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/file_utils.py +43 -35
  12. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/path_utils.py +32 -4
  13. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/allowed_checker.py +8 -0
  14. ostruct_cli-0.5.0/src/ostruct/cli/security/base.py +46 -0
  15. ostruct_cli-0.5.0/src/ostruct/cli/security/errors.py +164 -0
  16. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/security_manager.py +22 -9
  17. ostruct_cli-0.5.0/src/ostruct/cli/serialization.py +25 -0
  18. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_filters.py +5 -3
  19. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_rendering.py +46 -22
  20. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_utils.py +12 -4
  21. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_validation.py +26 -8
  22. ostruct_cli-0.5.0/src/ostruct/cli/token_utils.py +43 -0
  23. ostruct_cli-0.5.0/src/ostruct/cli/validators.py +109 -0
  24. ostruct_cli-0.4.0/src/ostruct/cli/click_options.py +0 -257
  25. ostruct_cli-0.4.0/src/ostruct/cli/errors.py +0 -380
  26. ostruct_cli-0.4.0/src/ostruct/cli/security/errors.py +0 -184
  27. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/LICENSE +0 -0
  28. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/__init__.py +0 -0
  29. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/__init__.py +0 -0
  30. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/cache_manager.py +0 -0
  31. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/progress.py +0 -0
  32. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/__init__.py +0 -0
  33. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/case_manager.py +0 -0
  34. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/normalization.py +0 -0
  35. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/safe_joiner.py +0 -0
  36. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/symlink_resolver.py +0 -0
  37. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/types.py +0 -0
  38. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/security/windows_paths.py +0 -0
  39. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_env.py +0 -0
  40. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_extensions.py +0 -0
  41. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_io.py +0 -0
  42. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_schema.py +0 -0
  43. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/cli/utils.py +0 -0
  44. {ostruct_cli-0.4.0 → ostruct_cli-0.5.0}/src/ostruct/py.typed +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ostruct-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: CLI for OpenAI Structured Output
5
5
  Author: Yaniv Golan
6
6
  Author-email: yaniv@golan.name
7
- Requires-Python: >=3.9,<4.0
7
+ Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
12
11
  Classifier: Programming Language :: Python :: 3.12
@@ -17,10 +16,10 @@ Requires-Dist: click (>=8.1.7,<9.0.0)
17
16
  Requires-Dist: ijson (>=3.2.3,<4.0.0)
18
17
  Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
19
18
  Requires-Dist: openai (>=1.0.0,<2.0.0)
20
- Requires-Dist: openai-structured (>=1.3.0,<2.0.0)
19
+ Requires-Dist: openai-structured (>=2.0.0,<3.0.0)
21
20
  Requires-Dist: pydantic (>=2.6.3,<3.0.0)
22
21
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
23
- Requires-Dist: tiktoken (>=0.8.0,<0.9.0)
22
+ Requires-Dist: tiktoken (>=0.9.0,<0.10.0)
24
23
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
25
24
  Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
26
25
  Requires-Dist: werkzeug (>=3.1.3,<4.0.0)
@@ -91,7 +90,16 @@ Extract information about the person: {{ stdin }}
91
90
  4. Run the CLI:
92
91
 
93
92
  ```bash
94
- echo "John Smith is a 35 year old software engineer" | ostruct --task @task.j2 --schema schema.json
93
+ ostruct run task.j2 schema.json
94
+ ```
95
+
96
+ Or with more options:
97
+
98
+ ```bash
99
+ ostruct run task.j2 schema.json \
100
+ -f content input.txt \
101
+ -m gpt-4o \
102
+ --sys-prompt "You are an expert content analyzer"
95
103
  ```
96
104
 
97
105
  Output:
@@ -112,15 +120,56 @@ Template files use the `.j2` extension to indicate they contain Jinja2 template
112
120
  - Makes it clear the file contains template logic
113
121
  - Follows industry standards for Jinja2 templates
114
122
 
115
- While the CLI accepts templates with any extension (when prefixed with `@`), we recommend using `.j2` for better tooling support and clarity.
123
+ ## CLI Options
124
+
125
+ The CLI revolves around a single subcommand called `run`. Basic usage:
126
+
127
+ ```bash
128
+ ostruct run <TASK_TEMPLATE> <SCHEMA_FILE> [OPTIONS]
129
+ ```
130
+
131
+ Common options include:
132
+
133
+ - File & Directory Inputs:
134
+ - `-f <NAME> <PATH>`: Map a single file to a variable name
135
+ - `-d <NAME> <DIR>`: Map a directory to a variable name
136
+ - `-p <NAME> <PATTERN>`: Map files matching a glob pattern to a variable name
137
+ - `-R, --recursive`: Enable recursive directory/pattern scanning
138
+
139
+ - Variables:
140
+ - `-V name=value`: Define a simple string variable
141
+ - `-J name='{"key":"value"}'`: Define a JSON variable
142
+
143
+ - Model Parameters:
144
+ - `-m, --model MODEL`: Select the OpenAI model (supported: gpt-4o, o1, o3-mini)
145
+ - `--temperature FLOAT`: Set sampling temperature (0.0-2.0)
146
+ - `--max-output-tokens INT`: Set maximum output tokens
147
+ - `--top-p FLOAT`: Set top-p sampling parameter (0.0-1.0)
148
+ - `--frequency-penalty FLOAT`: Adjust frequency penalty (-2.0-2.0)
149
+ - `--presence-penalty FLOAT`: Adjust presence penalty (-2.0-2.0)
150
+ - `--reasoning-effort [low|medium|high]`: Control model reasoning effort
151
+
152
+ - System Prompt:
153
+ - `--sys-prompt TEXT`: Provide system prompt directly
154
+ - `--sys-file FILE`: Load system prompt from file
155
+ - `--ignore-task-sysprompt`: Ignore system prompt in template frontmatter
156
+
157
+ - API Configuration:
158
+ - `--api-key KEY`: OpenAI API key (defaults to OPENAI_API_KEY env var)
159
+ - `--timeout FLOAT`: API timeout in seconds (default: 60.0)
116
160
 
117
161
  ## Debug Options
118
162
 
119
- - `--show-model-schema`: Display the generated Pydantic model schema
120
163
  - `--debug-validation`: Show detailed schema validation debugging
121
- - `--verbose-schema`: Enable verbose schema debugging output
122
- - `--debug-openai-stream`: Enable low-level debug output for OpenAI streaming (very verbose)
123
- - `--progress-level {none,basic,detailed}`: Set progress reporting level (default: basic)
164
+ - `--debug-openai-stream`: Enable low-level debug output for OpenAI streaming
165
+ - `--progress-level {none,basic,detailed}`: Set progress reporting level
166
+ - `none`: No progress indicators
167
+ - `basic`: Show key operation steps (default)
168
+ - `detailed`: Show all steps with additional info
169
+ - `--show-model-schema`: Display the generated Pydantic model schema
170
+ - `--verbose`: Enable verbose logging
171
+ - `--dry-run`: Validate and render template without making API calls
172
+ - `--no-progress`: Disable all progress indicators
124
173
 
125
174
  All debug and error logs are written to:
126
175
 
@@ -174,13 +223,3 @@ Contributions are welcome! Please feel free to submit a Pull Request.
174
223
 
175
224
  This project is licensed under the MIT License - see the LICENSE file for details.
176
225
 
177
- ## Migration from openai-structured
178
-
179
- If you were previously using the CLI bundled with openai-structured (pre-1.0.0), this is its new home. The migration is straightforward:
180
-
181
- 1. Update openai-structured to version 1.0.0 or later
182
- 2. Install ostruct-cli
183
- 3. Replace any `openai-structured` CLI commands with `ostruct`
184
-
185
- The functionality remains the same, just moved to a dedicated package for better maintenance and focus.
186
-
@@ -63,7 +63,16 @@ Extract information about the person: {{ stdin }}
63
63
  4. Run the CLI:
64
64
 
65
65
  ```bash
66
- echo "John Smith is a 35 year old software engineer" | ostruct --task @task.j2 --schema schema.json
66
+ ostruct run task.j2 schema.json
67
+ ```
68
+
69
+ Or with more options:
70
+
71
+ ```bash
72
+ ostruct run task.j2 schema.json \
73
+ -f content input.txt \
74
+ -m gpt-4o \
75
+ --sys-prompt "You are an expert content analyzer"
67
76
  ```
68
77
 
69
78
  Output:
@@ -84,15 +93,56 @@ Template files use the `.j2` extension to indicate they contain Jinja2 template
84
93
  - Makes it clear the file contains template logic
85
94
  - Follows industry standards for Jinja2 templates
86
95
 
87
- While the CLI accepts templates with any extension (when prefixed with `@`), we recommend using `.j2` for better tooling support and clarity.
96
+ ## CLI Options
97
+
98
+ The CLI revolves around a single subcommand called `run`. Basic usage:
99
+
100
+ ```bash
101
+ ostruct run <TASK_TEMPLATE> <SCHEMA_FILE> [OPTIONS]
102
+ ```
103
+
104
+ Common options include:
105
+
106
+ - File & Directory Inputs:
107
+ - `-f <NAME> <PATH>`: Map a single file to a variable name
108
+ - `-d <NAME> <DIR>`: Map a directory to a variable name
109
+ - `-p <NAME> <PATTERN>`: Map files matching a glob pattern to a variable name
110
+ - `-R, --recursive`: Enable recursive directory/pattern scanning
111
+
112
+ - Variables:
113
+ - `-V name=value`: Define a simple string variable
114
+ - `-J name='{"key":"value"}'`: Define a JSON variable
115
+
116
+ - Model Parameters:
117
+ - `-m, --model MODEL`: Select the OpenAI model (supported: gpt-4o, o1, o3-mini)
118
+ - `--temperature FLOAT`: Set sampling temperature (0.0-2.0)
119
+ - `--max-output-tokens INT`: Set maximum output tokens
120
+ - `--top-p FLOAT`: Set top-p sampling parameter (0.0-1.0)
121
+ - `--frequency-penalty FLOAT`: Adjust frequency penalty (-2.0-2.0)
122
+ - `--presence-penalty FLOAT`: Adjust presence penalty (-2.0-2.0)
123
+ - `--reasoning-effort [low|medium|high]`: Control model reasoning effort
124
+
125
+ - System Prompt:
126
+ - `--sys-prompt TEXT`: Provide system prompt directly
127
+ - `--sys-file FILE`: Load system prompt from file
128
+ - `--ignore-task-sysprompt`: Ignore system prompt in template frontmatter
129
+
130
+ - API Configuration:
131
+ - `--api-key KEY`: OpenAI API key (defaults to OPENAI_API_KEY env var)
132
+ - `--timeout FLOAT`: API timeout in seconds (default: 60.0)
88
133
 
89
134
  ## Debug Options
90
135
 
91
- - `--show-model-schema`: Display the generated Pydantic model schema
92
136
  - `--debug-validation`: Show detailed schema validation debugging
93
- - `--verbose-schema`: Enable verbose schema debugging output
94
- - `--debug-openai-stream`: Enable low-level debug output for OpenAI streaming (very verbose)
95
- - `--progress-level {none,basic,detailed}`: Set progress reporting level (default: basic)
137
+ - `--debug-openai-stream`: Enable low-level debug output for OpenAI streaming
138
+ - `--progress-level {none,basic,detailed}`: Set progress reporting level
139
+ - `none`: No progress indicators
140
+ - `basic`: Show key operation steps (default)
141
+ - `detailed`: Show all steps with additional info
142
+ - `--show-model-schema`: Display the generated Pydantic model schema
143
+ - `--verbose`: Enable verbose logging
144
+ - `--dry-run`: Validate and render template without making API calls
145
+ - `--no-progress`: Disable all progress indicators
96
146
 
97
147
  All debug and error logs are written to:
98
148
 
@@ -145,13 +195,3 @@ Contributions are welcome! Please feel free to submit a Pull Request.
145
195
  ## License
146
196
 
147
197
  This project is licensed under the MIT License - see the LICENSE file for details.
148
-
149
- ## Migration from openai-structured
150
-
151
- If you were previously using the CLI bundled with openai-structured (pre-1.0.0), this is its new home. The migration is straightforward:
152
-
153
- 1. Update openai-structured to version 1.0.0 or later
154
- 2. Install ostruct-cli
155
- 3. Replace any `openai-structured` CLI commands with `ostruct`
156
-
157
- The functionality remains the same, just moved to a dedicated package for better maintenance and focus.
@@ -4,27 +4,28 @@
4
4
 
5
5
  [tool.poetry]
6
6
  name = "ostruct-cli"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "CLI for OpenAI Structured Output"
9
9
  authors = ["Yaniv Golan <yaniv@golan.name>"]
10
10
  readme = "README.md"
11
11
  packages = [{include = "ostruct", from = "src"}]
12
+ include = ["py.typed"]
12
13
 
13
14
  [tool.poetry.dependencies]
14
- python = ">=3.9,<4.0"
15
+ python = ">=3.10,<4.0"
15
16
  pydantic = "^2.6.3"
16
17
  jsonschema = "^4.23.0"
17
18
  chardet = "^5.0.0"
18
19
  cachetools = "^5.3.2"
19
20
  ijson = "^3.2.3"
20
21
  typing-extensions = "^4.9.0"
21
- tiktoken = "^0.8.0"
22
+ tiktoken = "^0.9.0"
22
23
  pyyaml = "^6.0.2"
23
24
  tomli = {version = "^2.0.1", python = "<3.11"}
24
25
  click = "^8.1.7"
25
26
  werkzeug = "^3.1.3"
26
27
  openai = "^1.0.0"
27
- openai-structured = "^1.3.0"
28
+ openai-structured = "^2.0.0"
28
29
 
29
30
  [tool.poetry.scripts]
30
31
  ostruct = "ostruct.cli.cli:main"
@@ -68,26 +69,31 @@
68
69
 
69
70
  [tool.mypy]
70
71
  plugins = ["pydantic.mypy"]
71
- strict = true
72
- exclude = [
73
- "docs/",
74
- "examples/"
75
- ]
76
- packages = ["ostruct", "tests"]
77
- python_version = "3.9"
72
+ python_version = "3.10"
78
73
  warn_unused_configs = true
79
- disallow_untyped_defs = true
74
+ exclude = ["docs/*", "examples/*"]
75
+ disallow_untyped_defs = false
80
76
  check_untyped_defs = true
81
- warn_redundant_casts = true
82
- warn_unused_ignores = true
83
- warn_return_any = true
84
- warn_unreachable = true
77
+ warn_return_any = false
78
+ warn_unused_ignores = false
85
79
  show_error_codes = true
86
- ignore_missing_imports = false
80
+
81
+ # Stricter settings for source code
82
+ [[tool.mypy.overrides]]
83
+ module = "ostruct.*"
84
+ disallow_untyped_defs = true
85
+ warn_return_any = true
86
+ warn_unused_ignores = true
87
+
88
+ # Special handling for Click-related code
89
+ [[tool.mypy.overrides]]
90
+ module = ["click.*", "ostruct.cli.click_options"]
91
+ disallow_untyped_decorators = false
92
+ warn_return_any = false
87
93
 
88
94
  [tool.black]
89
95
  line-length = 79
90
- target-version = ["py39"]
96
+ target-version = ["py310"]
91
97
  include = '\.pyi?$'
92
98
  preview = false
93
99
  required-version = "24.8.0"
@@ -109,7 +115,7 @@
109
115
  asyncio_default_fixture_loop_scope = "function"
110
116
 
111
117
  [tool.ruff]
112
- target-version = "py39"
118
+ target-version = "py310"
113
119
 
114
120
  [tool.poetry.group.examples]
115
121
  optional = true
@@ -0,0 +1,183 @@
1
+ """Base error classes for CLI error handling."""
2
+
3
+ import logging
4
+ import socket
5
+ import sys
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Optional
8
+
9
+ import click
10
+
11
+ from .. import __version__
12
+ from .exit_codes import ExitCode
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CLIError(Exception):
18
+ """Base class for CLI errors.
19
+
20
+ Context Dictionary Conventions:
21
+ - schema_path: Path to schema file (preserved for compatibility)
22
+ - source: Origin of error (new standard, falls back to schema_path)
23
+ - path: File or directory path
24
+ - details: Additional structured error information
25
+ - timestamp: When the error occurred
26
+ - host: System hostname
27
+ - version: Software version
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ context: Optional[Dict[str, Any]] = None,
34
+ exit_code: int = ExitCode.INTERNAL_ERROR,
35
+ details: Optional[str] = None,
36
+ ) -> None:
37
+ """Initialize CLI error.
38
+
39
+ Args:
40
+ message: Error message
41
+ context: Optional context dictionary following conventions
42
+ exit_code: Exit code to use (defaults to INTERNAL_ERROR)
43
+ details: Optional detailed explanation of the error
44
+ """
45
+ super().__init__(message)
46
+ self.message = message
47
+ self.context = context or {}
48
+ self.exit_code = exit_code
49
+
50
+ # Add standard context fields
51
+ if details:
52
+ self.context["details"] = details
53
+
54
+ # Add runtime context
55
+ self.context.update(
56
+ {
57
+ "timestamp": datetime.utcnow().isoformat(),
58
+ "host": socket.gethostname(),
59
+ "version": __version__,
60
+ "python_version": sys.version.split()[0],
61
+ }
62
+ )
63
+
64
+ @property
65
+ def source(self) -> Optional[str]:
66
+ """Get error source with backward compatibility."""
67
+ return self.context.get("source") or self.context.get("schema_path")
68
+
69
+ def show(self, file: bool = True) -> None:
70
+ """Display error message to user.
71
+
72
+ Args:
73
+ file: Whether to write to stderr (True) or stdout (False)
74
+ """
75
+ click.secho(str(self), fg="red", err=file)
76
+
77
+ def __str__(self) -> str:
78
+ """Get string representation of error.
79
+
80
+ Format:
81
+ [ERROR_TYPE] Primary Message
82
+ Details: Explanation
83
+ Source: Origin
84
+ Path: /path/to/resource
85
+ Additional context fields...
86
+ Troubleshooting:
87
+ 1. First step
88
+ 2. Second step
89
+ """
90
+ # Get error type from exit code
91
+ error_type = ExitCode(self.exit_code).name
92
+
93
+ # Start with error type and message
94
+ lines = [f"[{error_type}] {self.message}"]
95
+
96
+ # Add details if present
97
+ if details := self.context.get("details"):
98
+ lines.append(f"Details: {details}")
99
+
100
+ # Add source and path information
101
+ if source := self.source:
102
+ lines.append(f"Source: {source}")
103
+ if path := self.context.get("path"):
104
+ lines.append(f"Path: {path}")
105
+
106
+ # Add any expanded path information
107
+ if original_path := self.context.get("original_path"):
108
+ lines.append(f"Original Path: {original_path}")
109
+ if expanded_path := self.context.get("expanded_path"):
110
+ lines.append(f"Expanded Path: {expanded_path}")
111
+ if base_dir := self.context.get("base_dir"):
112
+ lines.append(f"Base Directory: {base_dir}")
113
+ if allowed_dirs := self.context.get("allowed_dirs"):
114
+ if isinstance(allowed_dirs, list):
115
+ lines.append(f"Allowed Directories: {allowed_dirs}")
116
+ else:
117
+ lines.append(f"Allowed Directory: {allowed_dirs}")
118
+
119
+ # Add other context fields (excluding reserved ones)
120
+ reserved_keys = {
121
+ "source",
122
+ "details",
123
+ "schema_path",
124
+ "timestamp",
125
+ "host",
126
+ "version",
127
+ "python_version",
128
+ "troubleshooting",
129
+ "path",
130
+ "original_path",
131
+ "expanded_path",
132
+ "base_dir",
133
+ "allowed_dirs",
134
+ }
135
+ context_lines = []
136
+ for k, v in sorted(self.context.items()):
137
+ if k not in reserved_keys and v is not None:
138
+ # Convert key to title case and replace underscores with spaces
139
+ formatted_key = k.replace("_", " ").title()
140
+ context_lines.append(f"{formatted_key}: {v}")
141
+
142
+ if context_lines:
143
+ lines.extend(["", "Additional Information:"])
144
+ lines.extend(context_lines)
145
+
146
+ # Add troubleshooting tips if available
147
+ if tips := self.context.get("troubleshooting"):
148
+ lines.extend(["", "Troubleshooting:"])
149
+ if isinstance(tips, list):
150
+ lines.extend(f" {i+1}. {tip}" for i, tip in enumerate(tips))
151
+ else:
152
+ lines.append(f" 1. {tips}")
153
+
154
+ return "\n".join(lines)
155
+
156
+
157
+ class OstructFileNotFoundError(CLIError):
158
+ """Raised when a file is not found.
159
+
160
+ This is Ostruct's custom error for file not found scenarios, distinct from Python's built-in
161
+ FileNotFoundError. It provides additional context and troubleshooting information specific to
162
+ the CLI context.
163
+ """
164
+
165
+ def __init__(self, path: str, context: Optional[Dict[str, Any]] = None):
166
+ context = context or {}
167
+ context.update(
168
+ {
169
+ "details": "The specified file does not exist or cannot be accessed",
170
+ "path": path,
171
+ "troubleshooting": [
172
+ "Check if the file exists",
173
+ "Verify the path spelling is correct",
174
+ "Check file permissions",
175
+ "Ensure parent directories exist",
176
+ ],
177
+ }
178
+ )
179
+ super().__init__(
180
+ f"File not found: {path}",
181
+ exit_code=ExitCode.FILE_ERROR,
182
+ context=context,
183
+ )