paths examples

Basic Examples
Advanced Examples

Full Script Using paths

A complete file organizer that demonstrates cross-platform compatibility, advanced Skpath usage, autopath, and AnyPath.

"""
File Organizer

Organizes files by type into categorized directories.
Demonstrates:
- Cross-platform path handling
- autopath decorator
- AnyPath type hints
- Project root detection
- Path ID encoding
- File operations
"""

import json
import shutil
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path

from suitkaise import paths
from suitkaise.paths import (
    Skpath,
    AnyPath,
    autopath,
    PathDetectionError,
    streamline_path_quick,
)


# CONFIGURATION

# file type categories
FILE_CATEGORIES = {
    "images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"],
    "documents": [".pdf", ".doc", ".docx", ".txt", ".md", ".rst"],
    "code": [".py", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml"],
    "data": [".csv", ".xlsx", ".xls", ".sql", ".db"],
    "archives": [".zip", ".tar", ".gz", ".rar", ".7z"],
}


# DATA CLASSES

@dataclass
class OrganizedFile:
    """Record of a file that was organized."""
    original_path: str      # original relative path
    new_path: str           # new relative path
    category: str           # file category
    size: int               # file size in bytes
    organized_at: str       # ISO timestamp
    path_id: str            # reversible path ID


@dataclass
class OrganizeResult:
    """Result of an organize operation."""
    source_dir: str
    output_dir: str
    files_organized: int = 0
    files_skipped: int = 0
    errors: list[str] = field(default_factory=list)
    organized_files: list[OrganizedFile] = field(default_factory=list)


# HELPER FUNCTIONS
@autopath()
def get_category(file_path: Skpath) -> str | None:
    """
    Get the category for a file based on its extension.
    Returns None if file doesn't match any category.
    """
    suffix = file_path.suffix.lower()
    
    for category, extensions in FILE_CATEGORIES.items():
        if suffix in extensions:
            return category
    
    return None


@autopath()
def ensure_directory(path: Skpath) -> Skpath:
    """
    Ensure a directory exists, creating it if necessary.
    Uses autopath so you can pass str, Path, or Skpath.
    """
    if not path.exists:
        path.mkdir(parents=True, exist_ok=True)
    return path


@autopath()
def safe_copy(source: Skpath, dest: Skpath, overwrite: bool = False) -> Skpath | None:
    """
    Safely copy a file, handling name conflicts.
    Returns the destination path, or None if skipped.
    """
    # check if destination already exists
    if dest.exists and not overwrite:
        # generate unique name
        stem = dest.stem
        suffix = dest.suffix
        parent = dest.parent
        counter = 1
        
        while dest.exists:
            new_name = f"{stem}_{counter}{suffix}"
            dest = parent / new_name
            counter += 1
    
    # ensure parent directory exists
    ensure_directory(dest.parent)
    
    # copy the file
    return source.copy_to(dest, overwrite=overwrite)


# MAIN ORGANIZER CLASS

class FileOrganizer:
    """
    Organizes files from a source directory into categorized folders.
    
    Cross-platform compatible:
    - Uses Skpath for normalized path handling
    - Works on Windows, Mac, and Linux
    - Stores path IDs for database compatibility
    """
    
    def __init__(self, output_dir: AnyPath = None):
        """
        Initialize the organizer.
        
        Args:
            output_dir: Where to organize files to.
                       If None, uses "organized" in project root.
        """
        # get project root for default paths
        try:
            self.project_root = paths.get_project_root()
        except PathDetectionError:
            # no project root found - use current directory
            self.project_root = paths.get_cwd()
        
        # set output directory
        if output_dir is None:
            self.output_dir = self.project_root / "organized"
        else:
            self.output_dir = Skpath(output_dir)
        
        # ensure output directory exists
        ensure_directory(self.output_dir)
        
        # manifest file to track organized files
        self.manifest_path = self.output_dir / "manifest.json"
        self.manifest: list[OrganizedFile] = []
        
        # load existing manifest if present
        self._load_manifest()
    
    def _load_manifest(self):
        """Load the manifest file if it exists."""
        if self.manifest_path.exists:
            with open(self.manifest_path, "r") as f:
                data = json.load(f)
                self.manifest = [
                    OrganizedFile(**item) for item in data
                ]
    
    def _save_manifest(self):
        """Save the manifest file."""
        with open(self.manifest_path, "w") as f:
            json.dump(
                [vars(item) for item in self.manifest],
                f,
                indent=2
            )
    
    @autopath()
    def organize(
        self,
        source_dir: Skpath,
        recursive: bool = True,
        dry_run: bool = False
    ) -> OrganizeResult:
        """
        Organize files from source directory into categories.
        
        Args:
            source_dir: Directory to organize files from.
                       Accepts str, Path, or Skpath (via autopath).
            recursive: Whether to process subdirectories.
            dry_run: If True, don't actually move files.
        
        Returns:
            OrganizeResult with summary of operations.
        """
        result = OrganizeResult(
            source_dir=source_dir.rp,
            output_dir=self.output_dir.rp,
        )
        
        # get files to organize
        if recursive:
            files = list(source_dir.rglob("*"))
        else:
            files = list(source_dir.iterdir())
        
        for file_path in files:
            # skip directories
            if not file_path.is_file:
                continue
            
            # skip hidden files
            if file_path.name.startswith("."):
                result.files_skipped += 1
                continue
            
            # get category
            category = get_category(file_path)
            if category is None:
                result.files_skipped += 1
                continue
            
            # build destination path
            # sanitize filename for cross-platform compatibility
            safe_name = streamline_path_quick(file_path.name)
            dest_path = self.output_dir / category / safe_name
            
            if dry_run:
                # just record what would happen
                print(f"Would organize: {file_path.rp} → {dest_path.rp}")
                result.files_organized += 1
                continue
            
            # actually copy the file
            try:
                copied = safe_copy(file_path, dest_path)
                
                if copied:
                    # record the operation
                    record = OrganizedFile(
                        original_path=file_path.rp,
                        new_path=copied.rp,
                        category=category,
                        size=file_path.stat.st_size,
                        organized_at=datetime.now().isoformat(),
                        path_id=copied.id,  # store path ID for database
                    )
                    self.manifest.append(record)
                    result.organized_files.append(record)
                    result.files_organized += 1
                else:
                    result.files_skipped += 1
                    
            except Exception as e:
                result.errors.append(f"{file_path.rp}: {str(e)}")
        
        # save manifest
        if not dry_run:
            self._save_manifest()
        
        return result
    
    def get_file_by_id(self, path_id: str) -> Skpath | None:
        """
        Get an organized file by its path ID.
        
        Path IDs are reversible - you can reconstruct the full path.
        This is useful for database storage.
        """
        # find in manifest
        for record in self.manifest:
            if record.path_id == path_id:
                return Skpath(path_id)
        return None
    
    def get_files_by_category(self, category: str) -> list[Skpath]:
        """Get all organized files in a category."""
        return [
            Skpath(record.path_id)
            for record in self.manifest
            if record.category == category
        ]
    
    def print_summary(self):
        """Print a summary of organized files."""
        print(f"\n{'='*50}")
        print("File Organizer Summary")
        print(f"{'='*50}")
        print(f"Output directory: {self.output_dir.rp}")
        print(f"Total files: {len(self.manifest)}")
        print()
        
        # count by category
        by_category = {}
        for record in self.manifest:
            by_category[record.category] = by_category.get(record.category, 0) + 1
        
        print("By category:")
        for category, count in sorted(by_category.items()):
            print(f"  {category}: {count} files")
        
        # show directory tree
        print()
        print("Directory structure:")
        tree = paths.get_formatted_project_tree(
            root=self.output_dir,
            depth=2,
            include_files=False
        )
        print(tree)


# MAIN

def main():
    """Main entry point (self-contained)."""
    import tempfile
    
    with tempfile.TemporaryDirectory() as tmpdir:
        tmp_root = Skpath(tmpdir)
        source_dir = tmp_root / "downloads"
        output_dir = tmp_root / "organized"
        
        # seed some sample files
        (source_dir / "notes.txt").write_text("notes\n" * 10)
        (source_dir / "script.py").write_text("print('hello')\n")
        (source_dir / "data.json").write_text('{"ok": true}\n')
        (source_dir / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n")
        
        # create organizer with temp output directory
        organizer = FileOrganizer(output_dir=output_dir)
        
        print(f"Organizing files from: {source_dir.rp}")
        print(f"Output directory: {organizer.output_dir.rp}")
        print()
        
        # dry run
        print("Dry run:")
        result = organizer.organize(source_dir, dry_run=True)
        print(f"Would organize {result.files_organized} files")
        print(f"Would skip {result.files_skipped} files")
        print()
        
        # actually organize
        print("Organizing...")
        result = organizer.organize(source_dir, dry_run=False)
    
    # print results
        print(f"Organized: {result.files_organized} files")
        print(f"Skipped: {result.files_skipped} files")
        
        if result.errors:
            print(f"Errors: {len(result.errors)}")
            for error in result.errors[:5]:
                print(f"  - {error}")
        
        # show summary
        organizer.print_summary()
        
        # demonstrate path ID retrieval
        if organizer.manifest:
            print()
            print("Path ID demonstration:")
            record = organizer.manifest[0]
            print(f"  Original: {record.original_path}")
            print(f"  Path ID: {record.path_id}")
            
            # reconstruct path from ID
            reconstructed = Skpath(record.path_id)
            print(f"  Reconstructed: {reconstructed.rp}")


if __name__ == "__main__":
    main()