ostruct-cli 0.4.0__py3-none-any.whl → 0.6.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.
@@ -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
+ )