ostruct-cli 0.3.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 (47) hide show
  1. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/PKG-INFO +64 -24
  2. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/README.md +59 -19
  3. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/pyproject.toml +28 -20
  4. ostruct_cli-0.5.0/src/ostruct/cli/base_errors.py +183 -0
  5. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/cli.py +830 -585
  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.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/file_info.py +126 -69
  10. ostruct_cli-0.5.0/src/ostruct/cli/file_list.py +326 -0
  11. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/file_utils.py +132 -97
  12. ostruct_cli-0.5.0/src/ostruct/cli/path_utils.py +132 -0
  13. ostruct_cli-0.5.0/src/ostruct/cli/security/__init__.py +32 -0
  14. ostruct_cli-0.5.0/src/ostruct/cli/security/allowed_checker.py +55 -0
  15. ostruct_cli-0.5.0/src/ostruct/cli/security/base.py +46 -0
  16. ostruct_cli-0.5.0/src/ostruct/cli/security/case_manager.py +75 -0
  17. ostruct_cli-0.5.0/src/ostruct/cli/security/errors.py +164 -0
  18. ostruct_cli-0.5.0/src/ostruct/cli/security/normalization.py +161 -0
  19. ostruct_cli-0.5.0/src/ostruct/cli/security/safe_joiner.py +211 -0
  20. ostruct_cli-0.5.0/src/ostruct/cli/security/security_manager.py +366 -0
  21. ostruct_cli-0.5.0/src/ostruct/cli/security/symlink_resolver.py +483 -0
  22. ostruct_cli-0.5.0/src/ostruct/cli/security/types.py +108 -0
  23. ostruct_cli-0.5.0/src/ostruct/cli/security/windows_paths.py +404 -0
  24. ostruct_cli-0.5.0/src/ostruct/cli/serialization.py +25 -0
  25. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_filters.py +13 -8
  26. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_rendering.py +46 -22
  27. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_utils.py +12 -4
  28. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_validation.py +26 -8
  29. ostruct_cli-0.5.0/src/ostruct/cli/token_utils.py +43 -0
  30. ostruct_cli-0.5.0/src/ostruct/cli/validators.py +109 -0
  31. ostruct_cli-0.3.0/src/ostruct/cli/click_options.py +0 -248
  32. ostruct_cli-0.3.0/src/ostruct/cli/errors.py +0 -487
  33. ostruct_cli-0.3.0/src/ostruct/cli/file_list.py +0 -207
  34. ostruct_cli-0.3.0/src/ostruct/cli/path_utils.py +0 -123
  35. ostruct_cli-0.3.0/src/ostruct/cli/security.py +0 -964
  36. ostruct_cli-0.3.0/src/ostruct/cli/security_types.py +0 -46
  37. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/LICENSE +0 -0
  38. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/__init__.py +0 -0
  39. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/__init__.py +0 -0
  40. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/cache_manager.py +0 -0
  41. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/progress.py +0 -0
  42. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_env.py +0 -0
  43. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_extensions.py +0 -0
  44. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_io.py +0 -0
  45. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/template_schema.py +0 -0
  46. {ostruct_cli-0.3.0 → ostruct_cli-0.5.0}/src/ostruct/cli/utils.py +0 -0
  47. {ostruct_cli-0.3.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.3.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
@@ -16,10 +15,11 @@ Requires-Dist: chardet (>=5.0.0,<6.0.0)
16
15
  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
- Requires-Dist: openai-structured (>=1.0.0,<2.0.0)
18
+ Requires-Dist: openai (>=1.0.0,<2.0.0)
19
+ Requires-Dist: openai-structured (>=2.0.0,<3.0.0)
20
20
  Requires-Dist: pydantic (>=2.6.3,<3.0.0)
21
21
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
22
- Requires-Dist: tiktoken (>=0.8.0,<0.9.0)
22
+ Requires-Dist: tiktoken (>=0.9.0,<0.10.0)
23
23
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
24
24
  Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
25
25
  Requires-Dist: werkzeug (>=3.1.3,<4.0.0)
@@ -28,8 +28,8 @@ Description-Content-Type: text/markdown
28
28
  # ostruct-cli
29
29
 
30
30
  [![PyPI version](https://badge.fury.io/py/ostruct-cli.svg)](https://badge.fury.io/py/ostruct-cli)
31
- [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli/)
32
- [![Documentation Status](https://readthedocs.org/projects/ostruct-cli/badge/?version=latest)](https://ostruct-cli.readthedocs.io/en/latest/?badge=latest)
31
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli)
32
+ [![Documentation Status](https://readthedocs.org/projects/ostruct/badge/?version=latest)](https://ostruct.readthedocs.io/en/latest/?badge=latest)
33
33
  [![CI](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml/badge.svg)](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml)
34
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
35
 
@@ -90,7 +90,16 @@ Extract information about the person: {{ stdin }}
90
90
  4. Run the CLI:
91
91
 
92
92
  ```bash
93
- 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"
94
103
  ```
95
104
 
96
105
  Output:
@@ -111,22 +120,63 @@ Template files use the `.j2` extension to indicate they contain Jinja2 template
111
120
  - Makes it clear the file contains template logic
112
121
  - Follows industry standards for Jinja2 templates
113
122
 
114
- 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)
115
160
 
116
161
  ## Debug Options
117
162
 
118
- - `--show-model-schema`: Display the generated Pydantic model schema
119
163
  - `--debug-validation`: Show detailed schema validation debugging
120
- - `--verbose-schema`: Enable verbose schema debugging output
121
- - `--debug-openai-stream`: Enable low-level debug output for OpenAI streaming (very verbose)
122
- - `--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
123
173
 
124
174
  All debug and error logs are written to:
125
175
 
126
176
  - `~/.ostruct/logs/ostruct.log`: General application logs
127
177
  - `~/.ostruct/logs/openai_stream.log`: OpenAI streaming operations logs
128
178
 
129
- For more detailed documentation and examples, visit our [documentation](https://ostruct-cli.readthedocs.io/).
179
+ For more detailed documentation and examples, visit our [documentation](https://ostruct.readthedocs.io/).
130
180
 
131
181
  ## Development
132
182
 
@@ -173,13 +223,3 @@ Contributions are welcome! Please feel free to submit a Pull Request.
173
223
 
174
224
  This project is licensed under the MIT License - see the LICENSE file for details.
175
225
 
176
- ## Migration from openai-structured
177
-
178
- If you were previously using the CLI bundled with openai-structured (pre-1.0.0), this is its new home. The migration is straightforward:
179
-
180
- 1. Update openai-structured to version 1.0.0 or later
181
- 2. Install ostruct-cli
182
- 3. Replace any `openai-structured` CLI commands with `ostruct`
183
-
184
- The functionality remains the same, just moved to a dedicated package for better maintenance and focus.
185
-
@@ -1,8 +1,8 @@
1
1
  # ostruct-cli
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/ostruct-cli.svg)](https://badge.fury.io/py/ostruct-cli)
4
- [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli/)
5
- [![Documentation Status](https://readthedocs.org/projects/ostruct-cli/badge/?version=latest)](https://ostruct-cli.readthedocs.io/en/latest/?badge=latest)
4
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ostruct-cli.svg)](https://pypi.org/project/ostruct-cli)
5
+ [![Documentation Status](https://readthedocs.org/projects/ostruct/badge/?version=latest)](https://ostruct.readthedocs.io/en/latest/?badge=latest)
6
6
  [![CI](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml/badge.svg)](https://github.com/yaniv-golan/ostruct/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
@@ -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,22 +93,63 @@ 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
 
99
149
  - `~/.ostruct/logs/ostruct.log`: General application logs
100
150
  - `~/.ostruct/logs/openai_stream.log`: OpenAI streaming operations logs
101
151
 
102
- For more detailed documentation and examples, visit our [documentation](https://ostruct-cli.readthedocs.io/).
152
+ For more detailed documentation and examples, visit our [documentation](https://ostruct.readthedocs.io/).
103
153
 
104
154
  ## Development
105
155
 
@@ -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,26 +4,28 @@
4
4
 
5
5
  [tool.poetry]
6
6
  name = "ostruct-cli"
7
- version = "0.3.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
- openai-structured = "^1.0.0"
24
24
  tomli = {version = "^2.0.1", python = "<3.11"}
25
25
  click = "^8.1.7"
26
26
  werkzeug = "^3.1.3"
27
+ openai = "^1.0.0"
28
+ openai-structured = "^2.0.0"
27
29
 
28
30
  [tool.poetry.scripts]
29
31
  ostruct = "ostruct.cli.cli:main"
@@ -50,6 +52,7 @@
50
52
  types-cachetools = "^5.5.0.20240820"
51
53
  types-click = "^7.1.8"
52
54
  types-requests = "^2.32.0.20241016"
55
+ pre-commit = "^4.1.0"
53
56
 
54
57
  [tool.poetry.group.docs]
55
58
  optional = true
@@ -66,26 +69,31 @@
66
69
 
67
70
  [tool.mypy]
68
71
  plugins = ["pydantic.mypy"]
69
- strict = true
70
- exclude = [
71
- "docs/",
72
- "examples/"
73
- ]
74
- packages = ["ostruct", "tests"]
75
- python_version = "3.9"
72
+ python_version = "3.10"
76
73
  warn_unused_configs = true
77
- disallow_untyped_defs = true
74
+ exclude = ["docs/*", "examples/*"]
75
+ disallow_untyped_defs = false
78
76
  check_untyped_defs = true
79
- warn_redundant_casts = true
80
- warn_unused_ignores = true
81
- warn_return_any = true
82
- warn_unreachable = true
77
+ warn_return_any = false
78
+ warn_unused_ignores = false
83
79
  show_error_codes = true
84
- 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
85
93
 
86
94
  [tool.black]
87
95
  line-length = 79
88
- target-version = ["py39"]
96
+ target-version = ["py310"]
89
97
  include = '\.pyi?$'
90
98
  preview = false
91
99
  required-version = "24.8.0"
@@ -107,11 +115,11 @@
107
115
  asyncio_default_fixture_loop_scope = "function"
108
116
 
109
117
  [tool.ruff]
110
- target-version = "py39"
118
+ target-version = "py310"
111
119
 
112
120
  [tool.poetry.group.examples]
113
121
  optional = true
114
122
 
115
123
  [tool.poetry.group.examples.dependencies]
116
124
  tenacity = "^8.2.3"
117
- asyncio-throttle = "^1.0.2"
125
+ asyncio-throttle = "^1.0.2"
@@ -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
+ )