Source code for scitex_app.sdk._tree

#!/usr/bin/env python3
# Timestamp: 2026-03-15
# File: scitex_app/sdk/_tree.py

"""Tree builder utility for FilesBackend.

Converts flat file listings into nested directory tree structures.
Reusable by any app that needs a file browser UI.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, List, Optional


[docs] def build_tree( backend: Any, directory: str = "", *, extensions: Optional[List[str]] = None, skip_hidden: bool = True, max_depth: int = 10, ) -> List[Dict[str, Any]]: """Build a nested tree structure from a FilesBackend. Parameters ---------- backend : FilesBackend A file storage backend implementing the FilesBackend protocol. directory : str Starting directory (relative to backend root). Default: root. extensions : list of str, optional Filter files by extension (e.g., [".yaml", ".png"]). Directories are always included for traversal. skip_hidden : bool Skip files/directories starting with ".". Default: True. max_depth : int Maximum recursion depth to prevent runaway traversal. Default: 10. Returns ------- list of dict Nested tree structure:: [ {"path": "subdir", "name": "subdir", "type": "directory", "children": [...]}, {"path": "file.yaml", "name": "file.yaml", "type": "file"}, ] """ if max_depth <= 0: return [] # Get all entries in this directory entries = _list_entries(backend, directory) items = [] for entry in entries: name = Path(entry["path"]).name if skip_hidden and name.startswith("."): continue if entry["type"] == "directory": try: children = build_tree( backend, entry["path"], extensions=extensions, skip_hidden=skip_hidden, max_depth=max_depth - 1, ) except PermissionError: continue # skip directories we can't read if children: # only include non-empty directories items.append( { "path": entry["path"], "name": name, "type": "directory", "children": children, } ) else: # Apply extension filter to files if extensions: suffix = Path(name).suffix.lower() if suffix not in extensions: continue items.append( { "path": entry["path"], "name": name, "type": "file", } ) # Sort: pure alphabetical (case-insensitive), dirs and files interleaved items.sort(key=lambda x: x["name"].lower()) return items
def _list_entries( backend: Any, directory: str = "", ) -> List[Dict[str, str]]: """List directory entries with type information. Tries backend.list_entries() first (if available, returns files + dirs). Falls back to backend.list() for files and attempts directory discovery. """ # Preferred: backend supports list_entries() returning typed entries if hasattr(backend, "list_entries"): return backend.list_entries(directory) # Fallback: use list() for files, try to discover directories # via the backend's internal structure if hasattr(backend, "_root"): # FileSystemBackend — access pathlib directly for directory info root = backend._root target = (root / directory) if directory else root if not target.is_dir(): return [] entries = [] try: children = sorted(target.iterdir(), key=lambda x: x.name.lower()) except PermissionError: return [] for item in children: try: rel = str(item.relative_to(root)) if item.is_dir(): entries.append({"path": rel, "type": "directory"}) elif item.is_file(): entries.append({"path": rel, "type": "file"}) except PermissionError: continue # skip files/dirs we can't access return entries # Last resort: list() returns only files, no directory info files = backend.list(directory) return [{"path": f, "type": "file"} for f in files] # EOF