ostruct-cli 0.7.2__py3-none-any.whl → 0.8.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/__init__.py +21 -3
- ostruct/cli/base_errors.py +1 -1
- ostruct/cli/cli.py +66 -1983
- ostruct/cli/click_options.py +460 -28
- ostruct/cli/code_interpreter.py +238 -0
- ostruct/cli/commands/__init__.py +32 -0
- ostruct/cli/commands/list_models.py +128 -0
- ostruct/cli/commands/quick_ref.py +50 -0
- ostruct/cli/commands/run.py +137 -0
- ostruct/cli/commands/update_registry.py +71 -0
- ostruct/cli/config.py +277 -0
- ostruct/cli/cost_estimation.py +134 -0
- ostruct/cli/errors.py +310 -6
- ostruct/cli/exit_codes.py +1 -0
- ostruct/cli/explicit_file_processor.py +548 -0
- ostruct/cli/field_utils.py +69 -0
- ostruct/cli/file_info.py +42 -9
- ostruct/cli/file_list.py +301 -102
- ostruct/cli/file_search.py +455 -0
- ostruct/cli/file_utils.py +47 -13
- ostruct/cli/mcp_integration.py +541 -0
- ostruct/cli/model_creation.py +150 -1
- ostruct/cli/model_validation.py +204 -0
- ostruct/cli/progress_reporting.py +398 -0
- ostruct/cli/registry_updates.py +14 -9
- ostruct/cli/runner.py +1418 -0
- ostruct/cli/schema_utils.py +113 -0
- ostruct/cli/services.py +626 -0
- ostruct/cli/template_debug.py +748 -0
- ostruct/cli/template_debug_help.py +162 -0
- ostruct/cli/template_env.py +15 -6
- ostruct/cli/template_filters.py +55 -3
- ostruct/cli/template_optimizer.py +474 -0
- ostruct/cli/template_processor.py +1080 -0
- ostruct/cli/template_rendering.py +69 -34
- ostruct/cli/token_validation.py +286 -0
- ostruct/cli/types.py +78 -0
- ostruct/cli/unattended_operation.py +269 -0
- ostruct/cli/validators.py +386 -3
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
- ostruct_cli-0.8.0.dist-info/METADATA +633 -0
- ostruct_cli-0.8.0.dist-info/RECORD +69 -0
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.7.2.dist-info/METADATA +0 -370
- ostruct_cli-0.7.2.dist-info/RECORD +0 -45
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/file_info.py
CHANGED
@@ -24,6 +24,7 @@ class FileInfo:
|
|
24
24
|
content: Optional cached content
|
25
25
|
encoding: Optional cached encoding
|
26
26
|
hash_value: Optional cached hash value
|
27
|
+
routing_type: How the file was routed (e.g., 'template', 'code-interpreter')
|
27
28
|
"""
|
28
29
|
|
29
30
|
def __init__(
|
@@ -33,6 +34,7 @@ class FileInfo:
|
|
33
34
|
content: Optional[str] = None,
|
34
35
|
encoding: Optional[str] = None,
|
35
36
|
hash_value: Optional[str] = None,
|
37
|
+
routing_type: Optional[str] = None,
|
36
38
|
) -> None:
|
37
39
|
"""Initialize FileInfo instance.
|
38
40
|
|
@@ -42,19 +44,13 @@ class FileInfo:
|
|
42
44
|
content: Optional cached content
|
43
45
|
encoding: Optional cached encoding
|
44
46
|
hash_value: Optional cached hash value
|
47
|
+
routing_type: How the file was routed (e.g., 'template', 'code-interpreter')
|
45
48
|
|
46
49
|
Raises:
|
47
50
|
FileNotFoundError: If the file does not exist
|
48
51
|
PathSecurityError: If the path is not allowed
|
49
52
|
PermissionError: If access is denied
|
50
53
|
"""
|
51
|
-
logger.debug("Creating FileInfo for path: %s", path)
|
52
|
-
|
53
|
-
# Validate path
|
54
|
-
if not path:
|
55
|
-
raise ValueError("Path cannot be empty")
|
56
|
-
|
57
|
-
# Initialize private attributes
|
58
54
|
self.__path = str(path)
|
59
55
|
self.__security_manager = security_manager
|
60
56
|
self.__content = content
|
@@ -62,6 +58,17 @@ class FileInfo:
|
|
62
58
|
self.__hash = hash_value
|
63
59
|
self.__size: Optional[int] = None
|
64
60
|
self.__mtime: Optional[float] = None
|
61
|
+
self.routing_type = routing_type
|
62
|
+
|
63
|
+
logger.debug(
|
64
|
+
"Creating FileInfo for path: %s, routing_type: %s",
|
65
|
+
path,
|
66
|
+
self.routing_type,
|
67
|
+
)
|
68
|
+
|
69
|
+
# Validate path
|
70
|
+
if not path:
|
71
|
+
raise ValueError("Path cannot be empty")
|
65
72
|
|
66
73
|
try:
|
67
74
|
# This will raise PathSecurityError if path is not allowed
|
@@ -125,6 +132,23 @@ class FileInfo:
|
|
125
132
|
f"Permission denied: {os.path.basename(str(path))}"
|
126
133
|
) from e
|
127
134
|
|
135
|
+
# Add warning for large template-only files accessed via .content
|
136
|
+
# Check if routing_type is 'template' or if it's part of a legacy -f/-d mapping
|
137
|
+
# For simplicity now, let's assume if routing_type is None it could be legacy template
|
138
|
+
is_template_routed = (
|
139
|
+
self.routing_type == "template" or self.routing_type is None
|
140
|
+
)
|
141
|
+
if (
|
142
|
+
is_template_routed and self.size and self.size > 100 * 1024
|
143
|
+
): # 100KB threshold
|
144
|
+
logger.warning(
|
145
|
+
f"File '{self.path}' ({self.size / 1024:.1f}KB) was routed for template-only access "
|
146
|
+
f"but its .content is being accessed. This will include the entire file content "
|
147
|
+
f"in the prompt sent to the AI. For large files intended for analysis or search, "
|
148
|
+
f"consider using -fc (Code Interpreter) or -fs (File Search) to optimize token usage, "
|
149
|
+
f"cost, and avoid exceeding model context limits."
|
150
|
+
)
|
151
|
+
|
128
152
|
@property
|
129
153
|
def path(self) -> str:
|
130
154
|
"""Get the path relative to security manager's base directory.
|
@@ -356,13 +380,17 @@ class FileInfo:
|
|
356
380
|
|
357
381
|
@classmethod
|
358
382
|
def from_path(
|
359
|
-
cls,
|
383
|
+
cls,
|
384
|
+
path: str,
|
385
|
+
security_manager: SecurityManager,
|
386
|
+
routing_type: Optional[str] = None,
|
360
387
|
) -> "FileInfo":
|
361
388
|
"""Create FileInfo instance from path.
|
362
389
|
|
363
390
|
Args:
|
364
391
|
path: Path to file
|
365
392
|
security_manager: Security manager for path validation
|
393
|
+
routing_type: How the file was routed (e.g., 'template', 'code-interpreter')
|
366
394
|
|
367
395
|
Returns:
|
368
396
|
FileInfo instance
|
@@ -371,7 +399,7 @@ class FileInfo:
|
|
371
399
|
FileNotFoundError: If file does not exist
|
372
400
|
PathSecurityError: If path is not allowed
|
373
401
|
"""
|
374
|
-
return cls(path, security_manager)
|
402
|
+
return cls(path, security_manager, routing_type=routing_type)
|
375
403
|
|
376
404
|
def __str__(self) -> str:
|
377
405
|
"""String representation showing path."""
|
@@ -391,6 +419,11 @@ class FileInfo:
|
|
391
419
|
|
392
420
|
Internal methods can modify private attributes, but external access is prevented.
|
393
421
|
"""
|
422
|
+
# Allow setting routing_type if it's not already set (i.e., during __init__)
|
423
|
+
if name == "routing_type" and not hasattr(self, name):
|
424
|
+
object.__setattr__(self, name, value)
|
425
|
+
return
|
426
|
+
|
394
427
|
# Allow setting private attributes from internal methods
|
395
428
|
if name.startswith("_FileInfo__") and self._is_internal_call():
|
396
429
|
object.__setattr__(self, name, value)
|
ostruct/cli/file_list.py
CHANGED
@@ -2,7 +2,15 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
import threading
|
5
|
-
from typing import
|
5
|
+
from typing import (
|
6
|
+
Iterable,
|
7
|
+
Iterator,
|
8
|
+
List,
|
9
|
+
Optional,
|
10
|
+
SupportsIndex,
|
11
|
+
Union,
|
12
|
+
overload,
|
13
|
+
)
|
6
14
|
|
7
15
|
from .file_info import FileInfo
|
8
16
|
|
@@ -12,12 +20,15 @@ logger = logging.getLogger(__name__)
|
|
12
20
|
|
13
21
|
|
14
22
|
class FileInfoList(List[FileInfo]):
|
15
|
-
"""List of FileInfo objects with
|
23
|
+
"""List of FileInfo objects with strict single-file content access.
|
16
24
|
|
17
25
|
This class extends List[FileInfo] to provide convenient access to file contents
|
18
|
-
and metadata.
|
19
|
-
|
20
|
-
|
26
|
+
and metadata. Properties like content, path, name, etc. are designed for single-file
|
27
|
+
access only and will raise ValueError for multiple files or directory mappings.
|
28
|
+
|
29
|
+
This prevents accidental data exposure and template errors by requiring explicit
|
30
|
+
handling of multi-file scenarios through indexing (files[0].content) or the
|
31
|
+
|single filter (files|single.content).
|
21
32
|
|
22
33
|
This class is thread-safe. All operations that access or modify the internal list
|
23
34
|
are protected by a reentrant lock (RLock). This allows nested method calls while
|
@@ -30,195 +41,383 @@ class FileInfoList(List[FileInfo]):
|
|
30
41
|
files = FileInfoList([file_info], from_dir=False)
|
31
42
|
content = files.content # Returns "file contents"
|
32
43
|
|
33
|
-
Multiple files or directory (
|
44
|
+
Multiple files or directory (raises error):
|
34
45
|
files = FileInfoList([file1, file2]) # or FileInfoList([file1], from_dir=True)
|
35
|
-
content = files.content #
|
46
|
+
content = files.content # Raises ValueError with helpful message
|
36
47
|
|
37
|
-
|
38
|
-
content = files[0].content #
|
48
|
+
Safe multi-file access:
|
49
|
+
content = files[0].content # Access first file explicitly
|
50
|
+
content = files|single.content # Use |single filter for validation
|
39
51
|
|
40
52
|
Properties:
|
41
|
-
content: File content
|
42
|
-
path: File path
|
43
|
-
abs_path: Absolute file path
|
44
|
-
size: File size
|
53
|
+
content: File content - only for single file from file mapping (not directory)
|
54
|
+
path: File path - only for single file from file mapping
|
55
|
+
abs_path: Absolute file path - only for single file from file mapping
|
56
|
+
size: File size in bytes - only for single file from file mapping
|
57
|
+
name: Filename without directory path - only for single file from file mapping
|
58
|
+
names: Always returns list of all filenames (safe for multi-file access)
|
45
59
|
|
46
60
|
Raises:
|
47
|
-
ValueError: When accessing properties on
|
61
|
+
ValueError: When accessing scalar properties on empty list, multiple files, or directory mappings
|
48
62
|
"""
|
49
63
|
|
50
|
-
def __init__(
|
64
|
+
def __init__(
|
65
|
+
self,
|
66
|
+
files: List[FileInfo],
|
67
|
+
from_dir: bool = False,
|
68
|
+
var_alias: Optional[str] = None,
|
69
|
+
) -> None:
|
51
70
|
"""Initialize FileInfoList.
|
52
71
|
|
53
72
|
Args:
|
54
73
|
files: List of FileInfo objects
|
55
74
|
from_dir: Whether this list was created from a directory mapping
|
75
|
+
var_alias: Variable name used in template (for error messages)
|
56
76
|
"""
|
57
77
|
logger.debug(
|
58
|
-
"Creating FileInfoList with %d files, from_dir=%s",
|
78
|
+
"Creating FileInfoList with %d files, from_dir=%s, var_alias=%s",
|
59
79
|
len(files),
|
60
80
|
from_dir,
|
81
|
+
var_alias,
|
61
82
|
)
|
62
83
|
self._lock = threading.RLock() # Use RLock for nested method calls
|
63
84
|
super().__init__(files)
|
64
85
|
self._from_dir = from_dir
|
86
|
+
self._var_alias = var_alias
|
65
87
|
|
66
88
|
@property
|
67
|
-
def content(self) ->
|
68
|
-
"""Get the content of
|
89
|
+
def content(self) -> str:
|
90
|
+
"""Get the content of a single file.
|
69
91
|
|
70
92
|
Returns:
|
71
|
-
|
72
|
-
|
93
|
+
str: Content of the single file from file mapping.
|
94
|
+
|
95
|
+
Raises:
|
96
|
+
ValueError: If the list is empty or contains multiple files.
|
73
97
|
"""
|
74
98
|
# Take snapshot under lock
|
75
99
|
with self._lock:
|
76
100
|
if not self:
|
77
|
-
|
78
|
-
|
101
|
+
var_name = self._var_alias or "file_list"
|
102
|
+
raise ValueError(
|
103
|
+
f"No files in '{var_name}'. Cannot access .content property."
|
104
|
+
)
|
79
105
|
|
80
|
-
#
|
81
|
-
if len(self)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
106
|
+
# Check for multiple files or directory mapping
|
107
|
+
if len(self) > 1:
|
108
|
+
var_name = self._var_alias or "file_list"
|
109
|
+
raise ValueError(
|
110
|
+
f"'{var_name}' contains {len(self)} files. "
|
111
|
+
f"Use '{{{{ {var_name}[0].content }}}}' for the first file, "
|
112
|
+
f"'{{{{ {var_name}|single.content }}}}' if expecting exactly one file, "
|
113
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.content }}}}{{%% endfor %%}}'."
|
114
|
+
)
|
87
115
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
"
|
116
|
+
if self._from_dir:
|
117
|
+
var_name = self._var_alias or "file_list"
|
118
|
+
raise ValueError(
|
119
|
+
f"'{var_name}' contains files from directory mapping. "
|
120
|
+
f"Use '{{{{ {var_name}[0].content }}}}' for the first file, "
|
121
|
+
f"'{{{{ {var_name}|single.content }}}}' if expecting exactly one file, "
|
122
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.content }}}}{{%% endfor %%}}'."
|
93
123
|
)
|
94
|
-
return file_info.content
|
95
124
|
|
125
|
+
# Single file from file mapping
|
126
|
+
file_info = self[0]
|
127
|
+
|
128
|
+
# Access file content outside lock to prevent deadlocks
|
129
|
+
try:
|
96
130
|
logger.debug(
|
97
|
-
"FileInfoList.content returning
|
98
|
-
len(files),
|
99
|
-
self._from_dir,
|
131
|
+
"FileInfoList.content returning single file content (not from dir)"
|
100
132
|
)
|
101
|
-
return
|
133
|
+
return file_info.content
|
102
134
|
except Exception as e:
|
103
135
|
logger.error("Error accessing file content: %s", e)
|
104
136
|
raise
|
105
137
|
|
106
138
|
@property
|
107
|
-
def path(self) ->
|
108
|
-
"""Get the path of
|
139
|
+
def path(self) -> str:
|
140
|
+
"""Get the path of a single file.
|
109
141
|
|
110
142
|
Returns:
|
111
|
-
|
112
|
-
|
143
|
+
str: Path of the single file from file mapping.
|
144
|
+
|
145
|
+
Raises:
|
146
|
+
ValueError: If the list is empty or contains multiple files.
|
113
147
|
"""
|
114
148
|
# First get a snapshot of the list state under the lock
|
115
149
|
with self._lock:
|
116
150
|
if not self:
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
else:
|
122
|
-
files = list(self)
|
123
|
-
is_single = False
|
151
|
+
var_name = self._var_alias or "file_list"
|
152
|
+
raise ValueError(
|
153
|
+
f"No files in '{var_name}'. Cannot access .path property."
|
154
|
+
)
|
124
155
|
|
125
|
-
|
156
|
+
# Check for multiple files or directory mapping
|
157
|
+
if len(self) > 1:
|
158
|
+
var_name = self._var_alias or "file_list"
|
159
|
+
raise ValueError(
|
160
|
+
f"'{var_name}' contains {len(self)} files. "
|
161
|
+
f"Use '{{{{ {var_name}[0].path }}}}' for the first file, "
|
162
|
+
f"'{{{{ {var_name}|single.path }}}}' if expecting exactly one file, "
|
163
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.path }}}}{{%% endfor %%}}'."
|
164
|
+
)
|
165
|
+
|
166
|
+
if self._from_dir:
|
167
|
+
var_name = self._var_alias or "file_list"
|
168
|
+
raise ValueError(
|
169
|
+
f"'{var_name}' contains files from directory mapping. "
|
170
|
+
f"Use '{{{{ {var_name}[0].path }}}}' for the first file, "
|
171
|
+
f"'{{{{ {var_name}|single.path }}}}' if expecting exactly one file, "
|
172
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.path }}}}{{%% endfor %%}}'."
|
173
|
+
)
|
174
|
+
|
175
|
+
# Single file from file mapping
|
176
|
+
file_info = self[0]
|
177
|
+
|
178
|
+
# Now access file path outside the lock
|
126
179
|
try:
|
127
|
-
|
128
|
-
return file_info.path
|
129
|
-
return [f.path for f in files]
|
180
|
+
return file_info.path
|
130
181
|
except Exception as e:
|
131
182
|
logger.error("Error accessing file path: %s", e)
|
132
183
|
raise
|
133
184
|
|
134
185
|
@property
|
135
|
-
def abs_path(self) ->
|
136
|
-
"""Get the absolute path of
|
186
|
+
def abs_path(self) -> str:
|
187
|
+
"""Get the absolute path of a single file.
|
137
188
|
|
138
189
|
Returns:
|
139
|
-
|
140
|
-
For multiple files or directory mapping, returns a list of absolute paths.
|
190
|
+
str: Absolute path of the single file from file mapping.
|
141
191
|
|
142
192
|
Raises:
|
143
|
-
ValueError: If the list is empty
|
193
|
+
ValueError: If the list is empty or contains multiple files.
|
144
194
|
"""
|
145
195
|
# First get a snapshot of the list state under the lock
|
146
196
|
with self._lock:
|
147
197
|
if not self:
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
else:
|
153
|
-
files = list(self)
|
154
|
-
is_single = False
|
198
|
+
var_name = self._var_alias or "file_list"
|
199
|
+
raise ValueError(
|
200
|
+
f"No files in '{var_name}'. Cannot access .abs_path property."
|
201
|
+
)
|
155
202
|
|
156
|
-
|
203
|
+
# Check for multiple files or directory mapping
|
204
|
+
if len(self) > 1:
|
205
|
+
var_name = self._var_alias or "file_list"
|
206
|
+
raise ValueError(
|
207
|
+
f"'{var_name}' contains {len(self)} files. "
|
208
|
+
f"Use '{{{{ {var_name}[0].abs_path }}}}' for the first file, "
|
209
|
+
f"'{{{{ {var_name}|single.abs_path }}}}' if expecting exactly one file, "
|
210
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.abs_path }}}}{{%% endfor %%}}'."
|
211
|
+
)
|
212
|
+
|
213
|
+
if self._from_dir:
|
214
|
+
var_name = self._var_alias or "file_list"
|
215
|
+
raise ValueError(
|
216
|
+
f"'{var_name}' contains files from directory mapping. "
|
217
|
+
f"Use '{{{{ {var_name}[0].abs_path }}}}' for the first file, "
|
218
|
+
f"'{{{{ {var_name}|single.abs_path }}}}' if expecting exactly one file, "
|
219
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.abs_path }}}}{{%% endfor %%}}'."
|
220
|
+
)
|
221
|
+
|
222
|
+
# Single file from file mapping
|
223
|
+
file_info = self[0]
|
224
|
+
|
225
|
+
# Now access file path outside the lock
|
157
226
|
try:
|
158
|
-
|
159
|
-
return file_info.abs_path
|
160
|
-
return [f.abs_path for f in files]
|
227
|
+
return file_info.abs_path
|
161
228
|
except Exception as e:
|
162
229
|
logger.error("Error accessing absolute path: %s", e)
|
163
230
|
raise
|
164
231
|
|
165
232
|
@property
|
166
|
-
def size(self) ->
|
167
|
-
"""Get file size
|
233
|
+
def size(self) -> int:
|
234
|
+
"""Get file size of a single file in bytes.
|
168
235
|
|
169
236
|
Returns:
|
170
|
-
|
171
|
-
For multiple files or directory mapping, returns a list of sizes.
|
237
|
+
int: Size of the single file from file mapping in bytes.
|
172
238
|
|
173
239
|
Raises:
|
174
|
-
ValueError: If the list is empty
|
240
|
+
ValueError: If the list is empty, contains multiple files, or file size is None.
|
175
241
|
"""
|
176
242
|
# First get a snapshot of the list state under the lock
|
177
243
|
with self._lock:
|
178
244
|
if not self:
|
179
|
-
|
245
|
+
var_name = self._var_alias or "file_list"
|
246
|
+
raise ValueError(
|
247
|
+
f"No files in '{var_name}'. Cannot access .size property."
|
248
|
+
)
|
180
249
|
|
181
|
-
#
|
182
|
-
if len(self)
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
250
|
+
# Check for multiple files or directory mapping
|
251
|
+
if len(self) > 1:
|
252
|
+
var_name = self._var_alias or "file_list"
|
253
|
+
raise ValueError(
|
254
|
+
f"'{var_name}' contains {len(self)} files. "
|
255
|
+
f"Use '{{{{ {var_name}[0].size }}}}' for the first file, "
|
256
|
+
f"'{{{{ {var_name}|single.size }}}}' if expecting exactly one file, "
|
257
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.size }}}}{{%% endfor %%}}'."
|
258
|
+
)
|
188
259
|
|
189
|
-
|
260
|
+
if self._from_dir:
|
261
|
+
var_name = self._var_alias or "file_list"
|
262
|
+
raise ValueError(
|
263
|
+
f"'{var_name}' contains files from directory mapping. "
|
264
|
+
f"Use '{{{{ {var_name}[0].size }}}}' for the first file, "
|
265
|
+
f"'{{{{ {var_name}|single.size }}}}' if expecting exactly one file, "
|
266
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.size }}}}{{%% endfor %%}}'."
|
267
|
+
)
|
268
|
+
|
269
|
+
# Single file from file mapping
|
270
|
+
file_info = self[0]
|
271
|
+
|
272
|
+
# Now access file size outside the lock
|
190
273
|
try:
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
return size
|
198
|
-
|
199
|
-
sizes = []
|
200
|
-
for f in files:
|
201
|
-
size = f.size
|
202
|
-
if size is None:
|
203
|
-
raise ValueError(f"Could not get size for file: {f.path}")
|
204
|
-
sizes.append(size)
|
205
|
-
return sizes
|
274
|
+
size = file_info.size
|
275
|
+
if size is None:
|
276
|
+
raise ValueError(
|
277
|
+
f"Could not get size for file: {file_info.path}"
|
278
|
+
)
|
279
|
+
return size
|
206
280
|
except Exception as e:
|
207
281
|
logger.error("Error accessing file size: %s", e)
|
208
282
|
raise
|
209
283
|
|
284
|
+
@property
|
285
|
+
def name(self) -> str:
|
286
|
+
"""Get the filename of a single file without directory path.
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
str: Name of the single file from file mapping.
|
290
|
+
|
291
|
+
Raises:
|
292
|
+
ValueError: If the list is empty or contains multiple files.
|
293
|
+
"""
|
294
|
+
with self._lock:
|
295
|
+
if not self:
|
296
|
+
var_name = self._var_alias or "file_list"
|
297
|
+
raise ValueError(
|
298
|
+
f"No files in '{var_name}'. Cannot access .name property."
|
299
|
+
)
|
300
|
+
|
301
|
+
# Check for multiple files or directory mapping
|
302
|
+
if len(self) > 1:
|
303
|
+
var_name = self._var_alias or "file_list"
|
304
|
+
raise ValueError(
|
305
|
+
f"'{var_name}' contains {len(self)} files. "
|
306
|
+
f"Use '{{{{ {var_name}[0].name }}}}' for the first file, "
|
307
|
+
f"'{{{{ {var_name}|single.name }}}}' if expecting exactly one file, "
|
308
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.name }}}}{{%% endfor %%}}'."
|
309
|
+
)
|
310
|
+
|
311
|
+
if self._from_dir:
|
312
|
+
var_name = self._var_alias or "file_list"
|
313
|
+
raise ValueError(
|
314
|
+
f"'{var_name}' contains files from directory mapping. "
|
315
|
+
f"Use '{{{{ {var_name}[0].name }}}}' for the first file, "
|
316
|
+
f"'{{{{ {var_name}|single.name }}}}' if expecting exactly one file, "
|
317
|
+
f"or loop over files with '{{%% for file in {var_name} %%}}{{{{ file.name }}}}{{%% endfor %%}}'."
|
318
|
+
)
|
319
|
+
|
320
|
+
# Single file from file mapping
|
321
|
+
return self[0].name
|
322
|
+
|
323
|
+
@property
|
324
|
+
def names(self) -> List[str]:
|
325
|
+
"""Get all filenames as a list."""
|
326
|
+
with self._lock:
|
327
|
+
if not self:
|
328
|
+
return []
|
329
|
+
try:
|
330
|
+
return [f.name for f in self]
|
331
|
+
except Exception as e:
|
332
|
+
logger.error(
|
333
|
+
f"Error accessing file names for .names property in '{self._var_alias or 'FileInfoList'}': {e}"
|
334
|
+
)
|
335
|
+
raise
|
336
|
+
|
337
|
+
def __getattr__(self, attr_name: str) -> None:
|
338
|
+
"""Provide helpful error messages for FileInfo attributes accessed on multi-file lists."""
|
339
|
+
# Import here to avoid circular imports
|
340
|
+
try:
|
341
|
+
from .template_schema import FileInfoProxy
|
342
|
+
|
343
|
+
# Try to get _valid_attrs as a class attribute, but it's actually an instance attribute
|
344
|
+
# So we'll get an empty set and fall back to our hardcoded list
|
345
|
+
valid_attrs = getattr(FileInfoProxy, "_valid_attrs", None)
|
346
|
+
if not valid_attrs:
|
347
|
+
# Use the same attributes that FileInfoProxy uses
|
348
|
+
valid_attrs = {
|
349
|
+
"name",
|
350
|
+
"path",
|
351
|
+
"content",
|
352
|
+
"ext",
|
353
|
+
"basename",
|
354
|
+
"dirname",
|
355
|
+
"abs_path",
|
356
|
+
"exists",
|
357
|
+
"is_file",
|
358
|
+
"is_dir",
|
359
|
+
"size",
|
360
|
+
"mtime",
|
361
|
+
"encoding",
|
362
|
+
"hash",
|
363
|
+
"extension",
|
364
|
+
"parent",
|
365
|
+
"stem",
|
366
|
+
"suffix",
|
367
|
+
}
|
368
|
+
except ImportError:
|
369
|
+
# Fallback list of common FileInfo attributes
|
370
|
+
valid_attrs = {
|
371
|
+
"name",
|
372
|
+
"path",
|
373
|
+
"content",
|
374
|
+
"size",
|
375
|
+
"abs_path",
|
376
|
+
"exists",
|
377
|
+
"encoding",
|
378
|
+
"hash",
|
379
|
+
}
|
380
|
+
|
381
|
+
# Only provide enhanced errors for known FileInfo attributes
|
382
|
+
if attr_name in valid_attrs:
|
383
|
+
# Check if this is a non-scalar list trying to access FileInfo attributes
|
384
|
+
if len(self) != 1 or self._from_dir:
|
385
|
+
var_name = getattr(self, "_var_alias", None) or "file_list"
|
386
|
+
raise AttributeError(
|
387
|
+
f"'{var_name}' contains {len(self)} files. "
|
388
|
+
f"Use '{{{{ {var_name}[0].{attr_name} }}}}' for the first file, "
|
389
|
+
f"or '{{{{ {var_name}|single.{attr_name} }}}}' if expecting exactly one file."
|
390
|
+
)
|
391
|
+
|
392
|
+
# Let the default AttributeError occur for truly missing attributes
|
393
|
+
raise AttributeError(
|
394
|
+
f"'{type(self).__name__}' object has no attribute '{attr_name}'"
|
395
|
+
)
|
396
|
+
|
210
397
|
def __str__(self) -> str:
|
211
398
|
"""Get string representation of the file list.
|
212
399
|
|
213
400
|
Returns:
|
214
|
-
str:
|
401
|
+
str: Helpful guidance message for template usage
|
215
402
|
"""
|
216
403
|
with self._lock:
|
217
404
|
if not self:
|
218
405
|
return "FileInfoList([])"
|
406
|
+
|
407
|
+
# For single file from file mapping (--fta, -ft, etc.)
|
408
|
+
if len(self) == 1 and not self._from_dir:
|
409
|
+
var_name = self._var_alias or "file_var"
|
410
|
+
return f"[File '{self[0].path}' - Use {{ {var_name}.content }} to access file content]"
|
411
|
+
|
412
|
+
# For multiple files or directory mapping
|
413
|
+
var_name = self._var_alias or "file_list"
|
219
414
|
if len(self) == 1:
|
220
|
-
return f"
|
221
|
-
|
415
|
+
return f"[Directory file '{self[0].path}' - Use {{ {var_name}[0].content }} or {{ {var_name}|single.content }} to access content]"
|
416
|
+
else:
|
417
|
+
paths_preview = [f.path for f in self[:2]]
|
418
|
+
if len(self) > 2:
|
419
|
+
paths_preview.append(f"... +{len(self) - 2} more")
|
420
|
+
return f"[{len(self)} files: {', '.join(paths_preview)} - Use {{ {var_name}[0].content }} or loop over files]"
|
222
421
|
|
223
422
|
def __repr__(self) -> str:
|
224
423
|
"""Get detailed string representation of the file list.
|