ostruct-cli 0.4.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 +822 -543
- ostruct/cli/click_options.py +320 -202
- ostruct/cli/errors.py +222 -128
- ostruct/cli/exit_codes.py +18 -0
- ostruct/cli/file_info.py +30 -14
- ostruct/cli/file_list.py +4 -10
- ostruct/cli/file_utils.py +43 -35
- ostruct/cli/path_utils.py +32 -4
- ostruct/cli/security/allowed_checker.py +8 -0
- ostruct/cli/security/base.py +46 -0
- ostruct/cli/security/errors.py +83 -103
- ostruct/cli/security/security_manager.py +22 -9
- ostruct/cli/serialization.py +25 -0
- ostruct/cli/template_filters.py +5 -3
- 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.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/METADATA +60 -21
- ostruct_cli-0.5.0.dist-info/RECORD +42 -0
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.4.0.dist-info/RECORD +0 -36
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.4.0.dist-info → ostruct_cli-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -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
|
+
)
|