ostruct-cli 0.3.0__py3-none-any.whl → 0.5.0__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.
- ostruct/cli/base_errors.py +183 -0
- ostruct/cli/cli.py +830 -585
- ostruct/cli/click_options.py +338 -211
- ostruct/cli/errors.py +214 -227
- ostruct/cli/exit_codes.py +18 -0
- ostruct/cli/file_info.py +126 -69
- ostruct/cli/file_list.py +191 -72
- ostruct/cli/file_utils.py +132 -97
- ostruct/cli/path_utils.py +86 -77
- ostruct/cli/security/__init__.py +32 -0
- ostruct/cli/security/allowed_checker.py +55 -0
- ostruct/cli/security/base.py +46 -0
- ostruct/cli/security/case_manager.py +75 -0
- ostruct/cli/security/errors.py +164 -0
- ostruct/cli/security/normalization.py +161 -0
- ostruct/cli/security/safe_joiner.py +211 -0
- ostruct/cli/security/security_manager.py +366 -0
- ostruct/cli/security/symlink_resolver.py +483 -0
- ostruct/cli/security/types.py +108 -0
- ostruct/cli/security/windows_paths.py +404 -0
- ostruct/cli/serialization.py +25 -0
- ostruct/cli/template_filters.py +13 -8
- ostruct/cli/template_rendering.py +46 -22
- ostruct/cli/template_utils.py +12 -4
- ostruct/cli/template_validation.py +26 -8
- ostruct/cli/token_utils.py +43 -0
- ostruct/cli/validators.py +109 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/METADATA +64 -24
- ostruct_cli-0.5.0.dist-info/RECORD +42 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/WHEEL +1 -1
- ostruct/cli/security.py +0 -964
- ostruct/cli/security_types.py +0 -46
- ostruct_cli-0.3.0.dist-info/RECORD +0 -28
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.3.0.dist-info → ostruct_cli-0.5.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/errors.py
CHANGED
@@ -1,47 +1,14 @@
|
|
1
1
|
"""Custom error classes for CLI error handling."""
|
2
2
|
|
3
|
-
import
|
4
|
-
from
|
5
|
-
from typing import Any, Dict, List, Optional, TextIO, cast
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, List, Optional
|
6
5
|
|
7
|
-
import
|
6
|
+
from .base_errors import CLIError, OstructFileNotFoundError
|
7
|
+
from .exit_codes import ExitCode
|
8
|
+
from .security.base import SecurityErrorBase
|
9
|
+
from .security.errors import SecurityErrorReasons
|
8
10
|
|
9
|
-
|
10
|
-
class CLIError(click.ClickException):
|
11
|
-
"""Base class for all CLI errors."""
|
12
|
-
|
13
|
-
def __init__(self, message: str, context: Optional[Dict[str, Any]] = None):
|
14
|
-
super().__init__(message)
|
15
|
-
self.context = context or {}
|
16
|
-
self._has_been_logged = False # Use underscore for private attribute
|
17
|
-
|
18
|
-
@property
|
19
|
-
def has_been_logged(self) -> bool:
|
20
|
-
"""Whether this error has been logged."""
|
21
|
-
return self._has_been_logged
|
22
|
-
|
23
|
-
@has_been_logged.setter
|
24
|
-
def has_been_logged(self, value: bool) -> None:
|
25
|
-
"""Set whether this error has been logged."""
|
26
|
-
self._has_been_logged = value
|
27
|
-
|
28
|
-
def show(self, file: Optional[TextIO] = None) -> None:
|
29
|
-
"""Show the error message with optional context."""
|
30
|
-
if file is None:
|
31
|
-
file = cast(TextIO, click.get_text_stream("stderr"))
|
32
|
-
|
33
|
-
# Format message with context if available
|
34
|
-
if self.context:
|
35
|
-
context_str = "\n".join(
|
36
|
-
f" {k}: {v}" for k, v in self.context.items()
|
37
|
-
)
|
38
|
-
click.secho(
|
39
|
-
f"Error: {self.message}\nContext:\n{context_str}",
|
40
|
-
fg="red",
|
41
|
-
file=file,
|
42
|
-
)
|
43
|
-
else:
|
44
|
-
click.secho(f"Error: {self.message}", fg="red", file=file)
|
11
|
+
logger = logging.getLogger(__name__)
|
45
12
|
|
46
13
|
|
47
14
|
class VariableError(CLIError):
|
@@ -63,7 +30,7 @@ class VariableValueError(VariableError):
|
|
63
30
|
|
64
31
|
|
65
32
|
class InvalidJSONError(CLIError):
|
66
|
-
"""
|
33
|
+
"""Error raised when JSON is invalid."""
|
67
34
|
|
68
35
|
def __init__(
|
69
36
|
self,
|
@@ -71,236 +38,191 @@ class InvalidJSONError(CLIError):
|
|
71
38
|
source: Optional[str] = None,
|
72
39
|
context: Optional[Dict[str, Any]] = None,
|
73
40
|
):
|
41
|
+
"""Initialize invalid JSON error.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
message: Error message
|
45
|
+
source: Source of invalid JSON
|
46
|
+
context: Additional context for the error
|
47
|
+
"""
|
74
48
|
context = context or {}
|
75
49
|
if source:
|
76
50
|
context["source"] = source
|
77
|
-
super().__init__(
|
51
|
+
super().__init__(
|
52
|
+
message,
|
53
|
+
exit_code=ExitCode.DATA_ERROR,
|
54
|
+
context=context,
|
55
|
+
)
|
78
56
|
|
79
57
|
|
80
58
|
class PathError(CLIError):
|
81
59
|
"""Base class for path-related errors."""
|
82
60
|
|
83
61
|
def __init__(
|
84
|
-
self,
|
62
|
+
self,
|
63
|
+
message: str,
|
64
|
+
path: str,
|
65
|
+
context: Optional[Dict[str, Any]] = None,
|
66
|
+
exit_code: int = ExitCode.FILE_ERROR,
|
85
67
|
):
|
86
68
|
context = context or {}
|
87
69
|
context["path"] = path
|
88
|
-
super().__init__(message, context)
|
70
|
+
super().__init__(message, context=context, exit_code=exit_code)
|
89
71
|
|
90
72
|
|
91
|
-
class
|
92
|
-
"""Raised when a
|
73
|
+
class FileReadError(PathError):
|
74
|
+
"""Raised when a file cannot be read or decoded.
|
93
75
|
|
94
|
-
|
95
|
-
|
96
|
-
|
76
|
+
This is a wrapper exception that preserves the original cause (FileNotFoundError,
|
77
|
+
UnicodeDecodeError, etc) while providing a consistent interface for error handling.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(
|
81
|
+
self, message: str, path: str, context: Optional[Dict[str, Any]] = None
|
82
|
+
):
|
83
|
+
super().__init__(message, path, context)
|
97
84
|
|
98
85
|
|
99
86
|
class DirectoryNotFoundError(PathError):
|
100
87
|
"""Raised when a specified directory does not exist."""
|
101
88
|
|
102
89
|
def __init__(self, path: str, context: Optional[Dict[str, Any]] = None):
|
103
|
-
|
104
|
-
|
105
|
-
|
90
|
+
context = context or {}
|
91
|
+
context.update(
|
92
|
+
{
|
93
|
+
"details": "The specified directory does not exist or cannot be accessed",
|
94
|
+
"troubleshooting": [
|
95
|
+
"Check if the directory exists",
|
96
|
+
"Verify the path spelling is correct",
|
97
|
+
"Check directory permissions",
|
98
|
+
"Ensure parent directories exist",
|
99
|
+
"Use --allowed-dir to specify additional allowed directories",
|
100
|
+
],
|
101
|
+
}
|
102
|
+
)
|
103
|
+
super().__init__(
|
104
|
+
f"Directory not found: {path}", path=path, context=context
|
105
|
+
)
|
106
106
|
|
107
|
-
class PathSecurityError(PathError):
|
108
|
-
"""Exception raised when file access is denied due to security constraints.
|
109
107
|
|
110
|
-
|
111
|
-
|
112
|
-
wrapped: Whether this error has been wrapped by another error
|
113
|
-
"""
|
108
|
+
class PathSecurityError(SecurityErrorBase):
|
109
|
+
"""Error raised when a path violates security constraints."""
|
114
110
|
|
115
111
|
def __init__(
|
116
112
|
self,
|
117
113
|
message: str,
|
118
114
|
path: Optional[str] = None,
|
119
|
-
context: Optional[Dict[str, Any]] = None,
|
120
115
|
error_logged: bool = False,
|
121
|
-
|
122
|
-
|
116
|
+
wrapped: bool = False,
|
117
|
+
context: Optional[Dict[str, Any]] = None,
|
118
|
+
) -> None:
|
119
|
+
"""Initialize error.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
message: Error message
|
123
|
+
path: Path that caused the error
|
124
|
+
error_logged: Whether error has been logged
|
125
|
+
wrapped: Whether this is a wrapped error
|
126
|
+
context: Additional error context
|
127
|
+
"""
|
123
128
|
context = context or {}
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
return base
|
138
|
-
|
139
|
-
@property
|
140
|
-
def has_been_logged(self) -> bool:
|
141
|
-
"""Whether this error has been logged."""
|
142
|
-
return bool(self.context.get("has_been_logged", False))
|
129
|
+
if path is not None:
|
130
|
+
context["path"] = path
|
131
|
+
context.setdefault(
|
132
|
+
"details", "The specified path violates security constraints"
|
133
|
+
)
|
134
|
+
context.setdefault(
|
135
|
+
"troubleshooting",
|
136
|
+
[
|
137
|
+
"Check if the path is within allowed directories",
|
138
|
+
"Use --allowed-dir to specify additional allowed directories",
|
139
|
+
"Verify path permissions",
|
140
|
+
],
|
141
|
+
)
|
143
142
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
self.context["has_been_logged"] = value
|
143
|
+
super().__init__(message, context=context)
|
144
|
+
self._wrapped = wrapped
|
145
|
+
self._error_logged = error_logged
|
148
146
|
|
149
147
|
@property
|
150
148
|
def error_logged(self) -> bool:
|
151
|
-
"""
|
152
|
-
return self.
|
153
|
-
|
154
|
-
@error_logged.setter
|
155
|
-
def error_logged(self, value: bool) -> None:
|
156
|
-
"""Alias for has_been_logged for backward compatibility."""
|
157
|
-
self.has_been_logged = value
|
149
|
+
"""Whether this error has been logged."""
|
150
|
+
return self._error_logged
|
158
151
|
|
159
152
|
@property
|
160
153
|
def wrapped(self) -> bool:
|
161
|
-
"""Whether this
|
162
|
-
return
|
163
|
-
|
164
|
-
@wrapped.setter
|
165
|
-
def wrapped(self, value: bool) -> None:
|
166
|
-
"""Set whether this error has been wrapped."""
|
167
|
-
self.context["wrapped"] = value
|
168
|
-
|
169
|
-
@staticmethod
|
170
|
-
def _format_allowed_dirs(allowed_dirs: List[str]) -> str:
|
171
|
-
"""Format allowed directories as a list representation."""
|
172
|
-
return f"[{', '.join(repr(d) for d in allowed_dirs)}]"
|
173
|
-
|
174
|
-
@staticmethod
|
175
|
-
def _create_error_message(
|
176
|
-
path: str, base_dir: Optional[str] = None
|
177
|
-
) -> str:
|
178
|
-
"""Create a standardized error message."""
|
179
|
-
if base_dir:
|
180
|
-
rel_path = os.path.relpath(path, base_dir)
|
181
|
-
return f"Access denied: {rel_path} is outside base directory and not in allowed directories"
|
182
|
-
return f"Access denied: {path} is outside base directory and not in allowed directories"
|
154
|
+
"""Whether this is a wrapped error."""
|
155
|
+
return self._wrapped
|
183
156
|
|
184
157
|
@classmethod
|
185
158
|
def from_expanded_paths(
|
186
159
|
cls,
|
187
160
|
original_path: str,
|
188
161
|
expanded_path: str,
|
189
|
-
base_dir:
|
190
|
-
allowed_dirs:
|
162
|
+
base_dir: str,
|
163
|
+
allowed_dirs: List[str],
|
191
164
|
error_logged: bool = False,
|
192
165
|
) -> "PathSecurityError":
|
193
|
-
"""Create error with expanded path
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
166
|
+
"""Create error with expanded path information.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
original_path: Original path provided
|
170
|
+
expanded_path: Expanded absolute path
|
171
|
+
base_dir: Base directory
|
172
|
+
allowed_dirs: List of allowed directories
|
173
|
+
error_logged: Whether error has been logged
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
PathSecurityError instance
|
177
|
+
"""
|
198
178
|
context = {
|
199
179
|
"original_path": original_path,
|
200
180
|
"expanded_path": expanded_path,
|
201
|
-
"
|
181
|
+
"base_dir": base_dir,
|
182
|
+
"allowed_dirs": allowed_dirs,
|
183
|
+
"reason": SecurityErrorReasons.PATH_OUTSIDE_ALLOWED,
|
184
|
+
"details": "The path resolves to a location outside the allowed directories",
|
185
|
+
"troubleshooting": [
|
186
|
+
f"Ensure path is within base directory: {base_dir}",
|
187
|
+
"Use --allowed-dir to specify additional allowed directories",
|
188
|
+
f"Current allowed directories: {', '.join(allowed_dirs)}",
|
189
|
+
],
|
202
190
|
}
|
203
|
-
if base_dir:
|
204
|
-
context["base_dir"] = base_dir
|
205
|
-
if allowed_dirs:
|
206
|
-
context["allowed_dirs"] = allowed_dirs
|
207
|
-
|
208
|
-
# Format full message with all context
|
209
|
-
parts = [message]
|
210
|
-
if base_dir:
|
211
|
-
parts.append(f"Base directory: {base_dir}")
|
212
|
-
if allowed_dirs:
|
213
|
-
parts.append(
|
214
|
-
f"Allowed directories: {cls._format_allowed_dirs(allowed_dirs)}"
|
215
|
-
)
|
216
|
-
parts.append(
|
217
|
-
"Use --allowed-dir to specify additional allowed directories"
|
218
|
-
)
|
219
191
|
|
220
192
|
return cls(
|
221
|
-
"
|
193
|
+
f"Access denied: {original_path!r} resolves to {expanded_path!r} which is "
|
194
|
+
f"outside base directory {base_dir!r}",
|
222
195
|
path=original_path,
|
223
|
-
context=context,
|
224
196
|
error_logged=error_logged,
|
197
|
+
context=context,
|
225
198
|
)
|
226
199
|
|
227
200
|
@classmethod
|
228
|
-
def
|
229
|
-
|
230
|
-
path: Path,
|
231
|
-
reason: Optional[str] = None,
|
232
|
-
error_logged: bool = False,
|
233
|
-
) -> "PathSecurityError":
|
234
|
-
"""Create access denied error."""
|
235
|
-
msg = f"Access denied: {path}"
|
236
|
-
if reason:
|
237
|
-
msg += f" - {reason}"
|
238
|
-
msg += " is outside base directory and not in allowed directories"
|
239
|
-
return cls(msg, path=str(path), error_logged=error_logged)
|
201
|
+
def wrap_error(cls, msg: str, original: Exception) -> "PathSecurityError":
|
202
|
+
"""Wrap an existing error with additional context.
|
240
203
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
path: Path,
|
245
|
-
base_dir: Optional[Path] = None,
|
246
|
-
error_logged: bool = False,
|
247
|
-
) -> "PathSecurityError":
|
248
|
-
"""Create error for path outside allowed directories."""
|
249
|
-
msg = f"Access denied: {path} is outside base directory and not in allowed directories"
|
250
|
-
context = {}
|
251
|
-
if base_dir:
|
252
|
-
context["base_directory"] = str(base_dir)
|
253
|
-
return cls(
|
254
|
-
msg, path=str(path), context=context, error_logged=error_logged
|
255
|
-
)
|
204
|
+
Args:
|
205
|
+
msg: New error message
|
206
|
+
original: Original error to wrap
|
256
207
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
208
|
+
Returns:
|
209
|
+
New PathSecurityError instance
|
210
|
+
"""
|
211
|
+
context = {
|
212
|
+
"wrapped_error": type(original).__name__,
|
213
|
+
"original_message": str(original),
|
214
|
+
}
|
264
215
|
|
265
|
-
|
266
|
-
|
267
|
-
original_path: Optional[str] = None,
|
268
|
-
expanded_path: Optional[str] = None,
|
269
|
-
base_dir: Optional[str] = None,
|
270
|
-
allowed_dirs: Optional[List[str]] = None,
|
271
|
-
) -> str:
|
272
|
-
"""Format error message with additional context."""
|
273
|
-
parts = [self.message]
|
274
|
-
if original_path and expanded_path and original_path != expanded_path:
|
275
|
-
parts.append(f"Original path: {original_path}")
|
276
|
-
parts.append(f"Expanded path: {expanded_path}")
|
277
|
-
if base_dir:
|
278
|
-
parts.append(f"Base directory: {base_dir}")
|
279
|
-
if allowed_dirs:
|
280
|
-
parts.append(
|
281
|
-
f"Allowed directories: {self._format_allowed_dirs(allowed_dirs)}"
|
282
|
-
)
|
283
|
-
parts.append(
|
284
|
-
"Use --allowed-dir to specify additional allowed directories"
|
285
|
-
)
|
286
|
-
return "\n".join(parts)
|
216
|
+
if hasattr(original, "context"):
|
217
|
+
context.update(original.context)
|
287
218
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
new_context = original.context.copy()
|
295
|
-
new_context["wrapped"] = True # Mark as wrapped
|
296
|
-
error = cls(
|
297
|
-
message,
|
298
|
-
path=original.context.get("path", "unknown"),
|
299
|
-
context=new_context,
|
300
|
-
error_logged=original.has_been_logged,
|
219
|
+
return cls(
|
220
|
+
f"{msg}: {str(original)}",
|
221
|
+
path=getattr(original, "path", None),
|
222
|
+
error_logged=getattr(original, "error_logged", False),
|
223
|
+
wrapped=True,
|
224
|
+
context=context,
|
301
225
|
)
|
302
|
-
error.wrapped = True # Ensure wrapped is set through the property
|
303
|
-
return error
|
304
226
|
|
305
227
|
|
306
228
|
class TaskTemplateError(CLIError):
|
@@ -318,7 +240,22 @@ class TaskTemplateSyntaxError(TaskTemplateError):
|
|
318
240
|
class TaskTemplateVariableError(TaskTemplateError):
|
319
241
|
"""Raised when a task template uses undefined variables."""
|
320
242
|
|
321
|
-
|
243
|
+
def __init__(
|
244
|
+
self,
|
245
|
+
message: str,
|
246
|
+
context: Optional[Dict[str, Any]] = None,
|
247
|
+
) -> None:
|
248
|
+
"""Initialize error.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
message: Error message
|
252
|
+
context: Additional error context
|
253
|
+
"""
|
254
|
+
super().__init__(
|
255
|
+
message,
|
256
|
+
context=context,
|
257
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
258
|
+
)
|
322
259
|
|
323
260
|
|
324
261
|
class TemplateValidationError(TaskTemplateError):
|
@@ -340,18 +277,49 @@ class SchemaError(CLIError):
|
|
340
277
|
|
341
278
|
|
342
279
|
class SchemaFileError(CLIError):
|
343
|
-
"""
|
280
|
+
"""Error raised when schema file cannot be read."""
|
344
281
|
|
345
282
|
def __init__(
|
346
283
|
self,
|
347
284
|
message: str,
|
348
285
|
schema_path: Optional[str] = None,
|
349
286
|
context: Optional[Dict[str, Any]] = None,
|
350
|
-
):
|
287
|
+
) -> None:
|
288
|
+
"""Initialize schema file error.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
message: Error message
|
292
|
+
schema_path: Path to schema file
|
293
|
+
context: Additional context for the error
|
294
|
+
"""
|
351
295
|
context = context or {}
|
352
|
-
if schema_path:
|
296
|
+
if schema_path and "source" not in context:
|
353
297
|
context["schema_path"] = schema_path
|
354
|
-
|
298
|
+
context["source"] = schema_path # Use new standard field
|
299
|
+
context.setdefault(
|
300
|
+
"details",
|
301
|
+
"The schema file could not be read or contains errors",
|
302
|
+
)
|
303
|
+
context.setdefault(
|
304
|
+
"troubleshooting",
|
305
|
+
[
|
306
|
+
"Verify the schema file exists",
|
307
|
+
"Check if the schema file contains valid JSON",
|
308
|
+
"Ensure the schema follows the correct format",
|
309
|
+
"Check file permissions",
|
310
|
+
],
|
311
|
+
)
|
312
|
+
|
313
|
+
super().__init__(
|
314
|
+
message,
|
315
|
+
context=context,
|
316
|
+
exit_code=ExitCode.SCHEMA_ERROR,
|
317
|
+
)
|
318
|
+
|
319
|
+
@property
|
320
|
+
def schema_path(self) -> Optional[str]:
|
321
|
+
"""Get the schema path."""
|
322
|
+
return self.context.get("schema_path")
|
355
323
|
|
356
324
|
|
357
325
|
class SchemaValidationError(CLIError):
|
@@ -366,7 +334,23 @@ class SchemaValidationError(CLIError):
|
|
366
334
|
context = context or {}
|
367
335
|
if schema_path:
|
368
336
|
context["schema_path"] = schema_path
|
369
|
-
|
337
|
+
context["source"] = schema_path
|
338
|
+
context.setdefault("details", "The schema validation failed")
|
339
|
+
context.setdefault(
|
340
|
+
"troubleshooting",
|
341
|
+
[
|
342
|
+
"Check if the schema follows JSON Schema specification",
|
343
|
+
"Verify all required fields are present",
|
344
|
+
"Ensure field types are correctly specified",
|
345
|
+
"Check for any syntax errors in the schema",
|
346
|
+
],
|
347
|
+
)
|
348
|
+
|
349
|
+
super().__init__(
|
350
|
+
message,
|
351
|
+
context=context,
|
352
|
+
exit_code=ExitCode.SCHEMA_ERROR,
|
353
|
+
)
|
370
354
|
|
371
355
|
|
372
356
|
class ModelCreationError(CLIError):
|
@@ -415,12 +399,6 @@ class ModelNotSupportedError(CLIError):
|
|
415
399
|
pass
|
416
400
|
|
417
401
|
|
418
|
-
class ModelVersionError(CLIError):
|
419
|
-
"""Exception raised when a model version is not supported."""
|
420
|
-
|
421
|
-
pass
|
422
|
-
|
423
|
-
|
424
402
|
class StreamInterruptedError(CLIError):
|
425
403
|
"""Exception raised when a stream is interrupted."""
|
426
404
|
|
@@ -458,25 +436,34 @@ class InvalidResponseFormatError(CLIError):
|
|
458
436
|
|
459
437
|
|
460
438
|
class OpenAIClientError(CLIError):
|
461
|
-
"""Exception raised when there's an error with the OpenAI client.
|
439
|
+
"""Exception raised when there's an error with the OpenAI client.
|
462
440
|
|
463
|
-
|
441
|
+
This is a wrapper around openai_structured's OpenAIClientError to maintain
|
442
|
+
compatibility with our CLI error handling.
|
443
|
+
"""
|
444
|
+
|
445
|
+
def __init__(
|
446
|
+
self,
|
447
|
+
message: str,
|
448
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
449
|
+
context: Optional[Dict[str, Any]] = None,
|
450
|
+
):
|
451
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
464
452
|
|
465
453
|
|
466
454
|
# Export public API
|
467
455
|
__all__ = [
|
468
|
-
"CLIError",
|
469
456
|
"VariableError",
|
470
457
|
"PathError",
|
471
458
|
"PathSecurityError",
|
472
|
-
"
|
459
|
+
"OstructFileNotFoundError",
|
460
|
+
"FileReadError",
|
473
461
|
"DirectoryNotFoundError",
|
474
462
|
"SchemaValidationError",
|
475
463
|
"SchemaFileError",
|
476
464
|
"InvalidJSONError",
|
477
465
|
"ModelCreationError",
|
478
466
|
"ModelNotSupportedError",
|
479
|
-
"ModelVersionError",
|
480
467
|
"StreamInterruptedError",
|
481
468
|
"StreamBufferError",
|
482
469
|
"StreamParseError",
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""Exit codes for CLI operations."""
|
2
|
+
|
3
|
+
from enum import IntEnum
|
4
|
+
|
5
|
+
|
6
|
+
class ExitCode(IntEnum):
|
7
|
+
"""Exit codes for CLI operations."""
|
8
|
+
|
9
|
+
SUCCESS = 0
|
10
|
+
INTERNAL_ERROR = 1
|
11
|
+
USAGE_ERROR = 2
|
12
|
+
DATA_ERROR = 3
|
13
|
+
VALIDATION_ERROR = 4
|
14
|
+
API_ERROR = 5
|
15
|
+
SCHEMA_ERROR = 6
|
16
|
+
UNKNOWN_ERROR = 7
|
17
|
+
SECURITY_ERROR = 8
|
18
|
+
FILE_ERROR = 9
|