ostruct-cli 0.1.4__py3-none-any.whl → 0.3.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/file_list.py CHANGED
@@ -1,11 +1,14 @@
1
1
  """FileInfoList implementation providing smart file content access."""
2
2
 
3
- from typing import List, Union
3
+ import logging
4
+ from typing import Iterator, List, SupportsIndex, Union, overload
4
5
 
5
6
  from .file_info import FileInfo
6
7
 
7
8
  __all__ = ["FileInfoList", "FileInfo"]
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
 
10
13
  class FileInfoList(List[FileInfo]):
11
14
  """List of FileInfo objects with smart content access.
@@ -44,6 +47,11 @@ class FileInfoList(List[FileInfo]):
44
47
  files: List of FileInfo objects
45
48
  from_dir: Whether this list was created from a directory mapping
46
49
  """
50
+ logger.debug(
51
+ "Creating FileInfoList with %d files, from_dir=%s",
52
+ len(files),
53
+ from_dir,
54
+ )
47
55
  super().__init__(files)
48
56
  self._from_dir = from_dir
49
57
 
@@ -59,9 +67,18 @@ class FileInfoList(List[FileInfo]):
59
67
  ValueError: If the list is empty
60
68
  """
61
69
  if not self:
70
+ logger.debug("FileInfoList.content called but list is empty")
62
71
  raise ValueError("No files in FileInfoList")
63
72
  if len(self) == 1 and not self._from_dir:
73
+ logger.debug(
74
+ "FileInfoList.content returning single file content (not from dir)"
75
+ )
64
76
  return self[0].content
77
+ logger.debug(
78
+ "FileInfoList.content returning list of %d contents (from_dir=%s)",
79
+ len(self),
80
+ self._from_dir,
81
+ )
65
82
  return [f.content for f in self]
66
83
 
67
84
  @property
@@ -149,3 +166,42 @@ class FileInfoList(List[FileInfo]):
149
166
  str: Same as str() for consistency
150
167
  """
151
168
  return str(self)
169
+
170
+ def __iter__(self) -> Iterator[FileInfo]:
171
+ """Return iterator over files."""
172
+ logger.debug(
173
+ "Starting iteration over FileInfoList with %d files", len(self)
174
+ )
175
+ return super().__iter__()
176
+
177
+ def __len__(self) -> int:
178
+ """Return number of files."""
179
+ return super().__len__()
180
+
181
+ def __bool__(self) -> bool:
182
+ """Return True if there are files."""
183
+ return super().__len__() > 0
184
+
185
+ @overload
186
+ def __getitem__(self, index: SupportsIndex, /) -> FileInfo: ...
187
+
188
+ @overload
189
+ def __getitem__(self, index: slice, /) -> "FileInfoList": ...
190
+
191
+ def __getitem__(
192
+ self, index: Union[SupportsIndex, slice], /
193
+ ) -> Union[FileInfo, "FileInfoList"]:
194
+ """Get file at index."""
195
+ logger.debug("Getting file at index %s", index)
196
+ result = super().__getitem__(index)
197
+ if isinstance(index, slice):
198
+ if not isinstance(result, list):
199
+ raise TypeError(
200
+ f"Expected list from slice, got {type(result)}"
201
+ )
202
+ return FileInfoList(result, self._from_dir)
203
+ if not isinstance(result, FileInfo):
204
+ raise TypeError(
205
+ f"Expected FileInfo from index, got {type(result)}"
206
+ )
207
+ return result
ostruct/cli/file_utils.py CHANGED
@@ -132,7 +132,7 @@ def collect_files_from_pattern(
132
132
  return []
133
133
 
134
134
  # Create FileInfo objects
135
- files = []
135
+ files: List[FileInfo] = []
136
136
  for path in matched_paths:
137
137
  try:
138
138
  file_info = FileInfo.from_path(path, security_manager)
@@ -169,37 +169,77 @@ def collect_files_from_directory(
169
169
  DirectoryNotFoundError: If directory does not exist
170
170
  PathSecurityError: If directory is not allowed
171
171
  """
172
+ logger.debug(
173
+ "Collecting files from directory: %s (recursive=%s, extensions=%s)",
174
+ directory,
175
+ recursive,
176
+ allowed_extensions,
177
+ )
178
+
172
179
  # Validate directory exists and is allowed
173
180
  try:
174
181
  abs_dir = str(security_manager.resolve_path(directory))
175
- except PathSecurityError:
182
+ logger.debug("Resolved absolute directory path: %s", abs_dir)
183
+ logger.debug(
184
+ "Security manager base_dir: %s", security_manager.base_dir
185
+ )
186
+ logger.debug(
187
+ "Security manager allowed_dirs: %s", security_manager.allowed_dirs
188
+ )
189
+ except PathSecurityError as e:
190
+ logger.debug("PathSecurityError while resolving directory: %s", str(e))
176
191
  # Let the original error propagate
177
192
  raise
178
193
 
179
194
  if not os.path.exists(abs_dir):
195
+ logger.debug("Directory not found: %s (abs: %s)", directory, abs_dir)
180
196
  raise DirectoryNotFoundError(f"Directory not found: {directory}")
181
197
  if not os.path.isdir(abs_dir):
198
+ logger.debug(
199
+ "Path is not a directory: %s (abs: %s)", directory, abs_dir
200
+ )
182
201
  raise DirectoryNotFoundError(f"Path is not a directory: {directory}")
183
202
 
184
203
  # Collect files
185
- files = []
186
- for root, _, filenames in os.walk(abs_dir):
204
+ files: List[FileInfo] = []
205
+ for root, dirs, filenames in os.walk(abs_dir):
206
+ logger.debug("Walking directory: %s", root)
207
+ logger.debug("Found subdirectories: %s", dirs)
208
+ logger.debug("Found files: %s", filenames)
209
+
187
210
  if not recursive and root != abs_dir:
211
+ logger.debug(
212
+ "Skipping subdirectory (non-recursive mode): %s", root
213
+ )
188
214
  continue
189
215
 
216
+ logger.debug("Scanning directory: %s", root)
217
+ logger.debug("Current files collected: %d", len(files))
190
218
  for filename in filenames:
191
219
  # Get relative path from base directory
192
220
  abs_path = os.path.join(root, filename)
193
221
  try:
194
222
  rel_path = os.path.relpath(abs_path, security_manager.base_dir)
195
- except ValueError:
223
+ logger.debug("Processing file: %s -> %s", abs_path, rel_path)
224
+ except ValueError as e:
196
225
  # Skip files that can't be made relative
226
+ logger.debug(
227
+ "Skipping file that can't be made relative: %s (error: %s)",
228
+ abs_path,
229
+ str(e),
230
+ )
197
231
  continue
198
232
 
199
233
  # Check extension if filter is specified
200
234
  if allowed_extensions is not None:
201
235
  ext = os.path.splitext(filename)[1].lstrip(".")
202
236
  if ext not in allowed_extensions:
237
+ logger.debug(
238
+ "Skipping file with non-matching extension: %s (ext=%s, allowed=%s)",
239
+ filename,
240
+ ext,
241
+ allowed_extensions,
242
+ )
203
243
  continue
204
244
 
205
245
  try:
@@ -207,10 +247,17 @@ def collect_files_from_directory(
207
247
  rel_path, security_manager=security_manager, **kwargs
208
248
  )
209
249
  files.append(file_info)
210
- except (FileNotFoundError, PathSecurityError):
250
+ logger.debug("Added file to list: %s", rel_path)
251
+ except (FileNotFoundError, PathSecurityError) as e:
211
252
  # Skip files that can't be accessed
253
+ logger.debug(
254
+ "Skipping inaccessible file: %s (error: %s)",
255
+ rel_path,
256
+ str(e),
257
+ )
212
258
  continue
213
259
 
260
+ logger.debug("Collected %d files from directory %s", len(files), directory)
214
261
  return files
215
262
 
216
263
 
@@ -272,18 +319,38 @@ def collect_files(
272
319
  PathSecurityError: If a path is outside the base directory
273
320
  DirectoryNotFoundError: If a directory is not found
274
321
  """
322
+ logger.debug(
323
+ "Collecting files (recursive=%s, extensions=%s):\n files=%s\n patterns=%s\n dirs=%s",
324
+ dir_recursive,
325
+ dir_extensions,
326
+ file_mappings,
327
+ pattern_mappings,
328
+ dir_mappings,
329
+ )
330
+
275
331
  if security_manager is None:
276
332
  security_manager = SecurityManager(base_dir=os.getcwd())
333
+ logger.debug(
334
+ "Created default security manager with base_dir=%s", os.getcwd()
335
+ )
336
+ else:
337
+ logger.debug(
338
+ "Using provided security manager with base_dir=%s and allowed_dirs=%s",
339
+ security_manager.base_dir,
340
+ security_manager.allowed_dirs,
341
+ )
277
342
 
278
343
  # Normalize extensions by removing leading dots
279
344
  if dir_extensions:
280
345
  dir_extensions = [ext.lstrip(".") for ext in dir_extensions]
346
+ logger.debug("Normalized extensions: %s", dir_extensions)
281
347
 
282
348
  files: Dict[str, FileInfoList] = {}
283
349
 
284
350
  # Process file mappings
285
351
  if file_mappings:
286
352
  for mapping in file_mappings:
353
+ logger.debug("Processing file mapping: %s", mapping)
287
354
  name, path = _validate_and_split_mapping(mapping, "file")
288
355
  if name in files:
289
356
  raise ValueError(f"Duplicate file mapping: {name}")
@@ -292,10 +359,12 @@ def collect_files(
292
359
  path, security_manager=security_manager, **kwargs
293
360
  )
294
361
  files[name] = FileInfoList([file_info], from_dir=False)
362
+ logger.debug("Added single file mapping: %s -> %s", name, path)
295
363
 
296
364
  # Process pattern mappings
297
365
  if pattern_mappings:
298
366
  for mapping in pattern_mappings:
367
+ logger.debug("Processing pattern mapping: %s", mapping)
299
368
  name, pattern = _validate_and_split_mapping(mapping, "pattern")
300
369
  if name in files:
301
370
  raise ValueError(f"Duplicate pattern mapping: {name}")
@@ -305,6 +374,7 @@ def collect_files(
305
374
  pattern, security_manager=security_manager, **kwargs
306
375
  )
307
376
  except PathSecurityError as e:
377
+ logger.debug("Security error in pattern mapping: %s", str(e))
308
378
  raise PathSecurityError(
309
379
  "Pattern mapping error: Access denied: "
310
380
  f"{pattern} is outside base directory and not in allowed directories"
@@ -315,14 +385,24 @@ def collect_files(
315
385
  continue
316
386
 
317
387
  files[name] = FileInfoList(matched_files, from_dir=False)
388
+ logger.debug(
389
+ "Added pattern mapping: %s -> %s (%d files)",
390
+ name,
391
+ pattern,
392
+ len(matched_files),
393
+ )
318
394
 
319
395
  # Process directory mappings
320
396
  if dir_mappings:
321
397
  for mapping in dir_mappings:
398
+ logger.debug("Processing directory mapping: %s", mapping)
322
399
  name, directory = _validate_and_split_mapping(mapping, "directory")
323
400
  if name in files:
324
401
  raise ValueError(f"Duplicate directory mapping: {name}")
325
402
 
403
+ logger.debug(
404
+ "Processing directory mapping: %s -> %s", name, directory
405
+ )
326
406
  try:
327
407
  dir_files = collect_files_from_directory(
328
408
  directory=directory,
@@ -332,11 +412,14 @@ def collect_files(
332
412
  **kwargs,
333
413
  )
334
414
  except PathSecurityError as e:
415
+ logger.debug("Security error in directory mapping: %s", str(e))
335
416
  raise PathSecurityError(
336
417
  "Directory mapping error: Access denied: "
337
- f"{directory} is outside base directory and not in allowed directories"
418
+ f"{directory} is outside base directory and not in allowed directories",
419
+ path=directory,
338
420
  ) from e
339
- except DirectoryNotFoundError:
421
+ except DirectoryNotFoundError as e:
422
+ logger.debug("Directory not found: %s", str(e))
340
423
  raise DirectoryNotFoundError(
341
424
  f"Directory not found: {directory}"
342
425
  )
@@ -346,10 +429,18 @@ def collect_files(
346
429
  files[name] = FileInfoList([], from_dir=True)
347
430
  else:
348
431
  files[name] = FileInfoList(dir_files, from_dir=True)
432
+ logger.debug(
433
+ "Added directory mapping: %s -> %s (%d files)",
434
+ name,
435
+ directory,
436
+ len(dir_files),
437
+ )
349
438
 
350
439
  if not files:
440
+ logger.debug("No files found in any mappings")
351
441
  raise ValueError("No files found")
352
442
 
443
+ logger.debug("Collected files total mappings: %d", len(files))
353
444
  return files
354
445
 
355
446