From c5690f99c76435b2d31afe7972aaa477fc31cf1b Mon Sep 17 00:00:00 2001 From: Shravan250 Date: Thu, 16 Oct 2025 22:32:01 +0530 Subject: [PATCH 1/5] add initial project structure with CLI tool and configuration files --- bulk-file-organizer/.gitignore | 3 +++ bulk-file-organizer/CONTRIBUTING.md | 0 bulk-file-organizer/README.md | 0 .../bulk_organizer/__init__.py | 0 bulk-file-organizer/bulk_organizer/actions.py | 0 .../bulk_file_organizer.egg-info/PKG-INFO | 11 ++++++++ .../bulk_file_organizer.egg-info/SOURCES.txt | 8 ++++++ .../dependency_links.txt | 1 + .../bulk_file_organizer.egg-info/requires.txt | 5 ++++ .../top_level.txt | 1 + bulk-file-organizer/bulk_organizer/cli.py | 25 +++++++++++++++++++ bulk-file-organizer/bulk_organizer/mapper.py | 0 bulk-file-organizer/bulk_organizer/scanner.py | 0 bulk-file-organizer/bulk_organizer/utils.py | 0 bulk-file-organizer/pyproject.toml | 22 ++++++++++++++++ bulk-file-organizer/tests/test_mapper.py | 0 16 files changed, 76 insertions(+) create mode 100644 bulk-file-organizer/.gitignore create mode 100644 bulk-file-organizer/CONTRIBUTING.md create mode 100644 bulk-file-organizer/README.md create mode 100644 bulk-file-organizer/bulk_organizer/__init__.py create mode 100644 bulk-file-organizer/bulk_organizer/actions.py create mode 100644 bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO create mode 100644 bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt create mode 100644 bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt create mode 100644 bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt create mode 100644 bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt create mode 100644 bulk-file-organizer/bulk_organizer/cli.py create mode 100644 bulk-file-organizer/bulk_organizer/mapper.py create mode 100644 bulk-file-organizer/bulk_organizer/scanner.py create mode 100644 bulk-file-organizer/bulk_organizer/utils.py create mode 100644 bulk-file-organizer/pyproject.toml create mode 100644 bulk-file-organizer/tests/test_mapper.py diff --git a/bulk-file-organizer/.gitignore b/bulk-file-organizer/.gitignore new file mode 100644 index 0000000..c622b7b --- /dev/null +++ b/bulk-file-organizer/.gitignore @@ -0,0 +1,3 @@ +venv/ +__pycache__/ +.pytest_cache/ \ No newline at end of file diff --git a/bulk-file-organizer/CONTRIBUTING.md b/bulk-file-organizer/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/README.md b/bulk-file-organizer/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/__init__.py b/bulk-file-organizer/bulk_organizer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/actions.py b/bulk-file-organizer/bulk_organizer/actions.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO new file mode 100644 index 0000000..7f12eec --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.4 +Name: bulk-file-organizer +Version: 0.1.0 +Summary: A CLI tool to organize files into subfolders based on file extensions. +Author: Shravan +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" +Requires-Dist: ruff; extra == "dev" +Requires-Dist: black; extra == "dev" diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt new file mode 100644 index 0000000..f482473 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.md +pyproject.toml +bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO +bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt +bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt +bulk_organizer/bulk_file_organizer.egg-info/requires.txt +bulk_organizer/bulk_file_organizer.egg-info/top_level.txt +tests/test_mapper.py \ No newline at end of file diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt new file mode 100644 index 0000000..5e0e4a0 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt @@ -0,0 +1,5 @@ + +[dev] +pytest +ruff +black diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt @@ -0,0 +1 @@ + diff --git a/bulk-file-organizer/bulk_organizer/cli.py b/bulk-file-organizer/bulk_organizer/cli.py new file mode 100644 index 0000000..bef8102 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/cli.py @@ -0,0 +1,25 @@ +import argparse +import tomllib +from pathlib import Path + +def get_version(): + toml_path = Path(__file__).resolve().parent.parent / "pyproject.toml" + with open(toml_path, "rb") as f: + pyproject = tomllib.load(f) + return pyproject["project"]["version"] + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}") + parser.add_argument("directory", type=str, help="Path to the directory to organize") + parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes") + args = parser.parse_args() + + print(f"Organizing: {args.directory}") + if args.dry_run: + print("Dry run mode enabled") + +if __name__ == "__main__": + main() diff --git a/bulk-file-organizer/bulk_organizer/mapper.py b/bulk-file-organizer/bulk_organizer/mapper.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/scanner.py b/bulk-file-organizer/bulk_organizer/scanner.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/utils.py b/bulk-file-organizer/bulk_organizer/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/pyproject.toml b/bulk-file-organizer/pyproject.toml new file mode 100644 index 0000000..9eebad3 --- /dev/null +++ b/bulk-file-organizer/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "bulk-file-organizer" +version = "0.1.0" +description = "A CLI tool to organize files into subfolders based on file extensions." +authors = [{ name = "Shravan" }] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", # optional linter + "black", # formatter +] + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["bulk_organizer"] diff --git a/bulk-file-organizer/tests/test_mapper.py b/bulk-file-organizer/tests/test_mapper.py new file mode 100644 index 0000000..e69de29 From 4bd0ec3ae42923cd56956350561aeab163fd39bb Mon Sep 17 00:00:00 2001 From: Shravan250 Date: Thu, 16 Oct 2025 23:17:44 +0530 Subject: [PATCH 2/5] implement scanner function with recursive and iterative option and add tests for scanning behavior --- bulk-file-organizer/.gitignore | 45 ++++++++++++++++++- bulk-file-organizer/bulk_organizer/cli.py | 15 ++++++- bulk-file-organizer/bulk_organizer/scanner.py | 25 +++++++++++ bulk-file-organizer/tests/test_mapper.py | 26 +++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/bulk-file-organizer/.gitignore b/bulk-file-organizer/.gitignore index c622b7b..6cd4d7f 100644 --- a/bulk-file-organizer/.gitignore +++ b/bulk-file-organizer/.gitignore @@ -1,3 +1,44 @@ -venv/ +# Byte-compiled / optimized / DLL files __pycache__/ -.pytest_cache/ \ No newline at end of file +*.py[cod] +*$py.class + +# Virtual environment +venv/ +.env/ +.venv/ + +# Pytest cache +.pytest_cache/ + +# VS Code settings +.vscode/ + +# macOS metadata +.DS_Store + +# Coverage reports +htmlcov/ +.coverage +coverage.xml +*.cover + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Logs and temp files +*.log +*.tmp +*.bak + +# MyPy and Ruff +.mypy_cache/ +.ruff_cache/ + +# Jupyter Notebook checkpoints (if any) +.ipynb_checkpoints/ + +# Test output +test-output/ diff --git a/bulk-file-organizer/bulk_organizer/cli.py b/bulk-file-organizer/bulk_organizer/cli.py index bef8102..3d00e1c 100644 --- a/bulk-file-organizer/bulk_organizer/cli.py +++ b/bulk-file-organizer/bulk_organizer/cli.py @@ -1,6 +1,8 @@ import argparse import tomllib from pathlib import Path +from bulk_organizer.scanner import scanner + def get_version(): toml_path = Path(__file__).resolve().parent.parent / "pyproject.toml" @@ -15,11 +17,22 @@ def main(): parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}") parser.add_argument("directory", type=str, help="Path to the directory to organize") parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes") + parser.add_argument("--recursive", action="store_true", default=True, help="Recursively scan subdirectories") + args = parser.parse_args() + directory_path = Path(args.directory) - print(f"Organizing: {args.directory}") + print(f"Organizing: {directory_path}") if args.dry_run: print("Dry run mode enabled") + + # Scan and print file paths + try: + for file_path in scanner(directory_path, recursive=args.recursive): + print(file_path) + except ValueError as e: + print(f"Error: {e}") + if __name__ == "__main__": main() diff --git a/bulk-file-organizer/bulk_organizer/scanner.py b/bulk-file-organizer/bulk_organizer/scanner.py index e69de29..02c38c9 100644 --- a/bulk-file-organizer/bulk_organizer/scanner.py +++ b/bulk-file-organizer/bulk_organizer/scanner.py @@ -0,0 +1,25 @@ +from pathlib import Path +from typing import Iterator + +def scanner(directory: Path, recursive: bool = True) -> Iterator[Path]: + """Scan the given directory and yield file paths. + + Args: + directory (Path): The directory to scan. + recursive (bool): Whether to scan subdirectories recursively. + + Yields: + Iterator[Path]: An iterator of file paths. + """ + + if not directory.exists() or not directory.is_dir(): + raise ValueError(f"{directory} is not a valid directory") + + if recursive: + yield from (p for p in directory.rglob("*") if p.is_file()) + else: + yield from (p for p in directory.iterdir() if p.is_file()) + + + + diff --git a/bulk-file-organizer/tests/test_mapper.py b/bulk-file-organizer/tests/test_mapper.py index e69de29..1cf4358 100644 --- a/bulk-file-organizer/tests/test_mapper.py +++ b/bulk-file-organizer/tests/test_mapper.py @@ -0,0 +1,26 @@ +import pytest +from pathlib import Path +from bulk_organizer.scanner import scanner + +def test_scanner_non_recursive(tmp_path): + # Setup: create files and folders + (tmp_path / "file1.txt").write_text("hello") + (tmp_path / "subdir").mkdir() + (tmp_path / "subdir" / "file2.txt").write_text("world") + + # Act + files = list(scanner(tmp_path, recursive=False)) + + # Assert + assert len(files) == 1 + assert files[0].name == "file1.txt" + +def test_scanner_recursive(tmp_path): + (tmp_path / "file1.txt").write_text("hello") + (tmp_path / "subdir").mkdir() + (tmp_path / "subdir" / "file2.txt").write_text("world") + + files = list(scanner(tmp_path, recursive=True)) + + assert len(files) == 2 + assert sorted([f.name for f in files]) == ["file1.txt", "file2.txt"] From f05ed11f271e02a6c57ee9e4e63dd145b2dc79f8 Mon Sep 17 00:00:00 2001 From: Shravan250 Date: Fri, 17 Oct 2025 00:06:49 +0530 Subject: [PATCH 3/5] add mapping functionality for file extensions and implement summary option in CLI to simulate dry-run --- bulk-file-organizer/.gitignore | 1 + bulk-file-organizer/bulk_organizer/cli.py | 22 ++++++++++++++- bulk-file-organizer/bulk_organizer/mapper.py | 27 +++++++++++++++++++ .../{actions.py => organizer.py} | 0 bulk-file-organizer/bulk_organizer/utils.py | 2 ++ bulk-file-organizer/tests/test_mapper.py | 10 +++++++ 6 files changed, 61 insertions(+), 1 deletion(-) rename bulk-file-organizer/bulk_organizer/{actions.py => organizer.py} (100%) diff --git a/bulk-file-organizer/.gitignore b/bulk-file-organizer/.gitignore index 6cd4d7f..a650ac9 100644 --- a/bulk-file-organizer/.gitignore +++ b/bulk-file-organizer/.gitignore @@ -42,3 +42,4 @@ dist/ # Test output test-output/ +test/ \ No newline at end of file diff --git a/bulk-file-organizer/bulk_organizer/cli.py b/bulk-file-organizer/bulk_organizer/cli.py index 3d00e1c..4de21b9 100644 --- a/bulk-file-organizer/bulk_organizer/cli.py +++ b/bulk-file-organizer/bulk_organizer/cli.py @@ -2,6 +2,7 @@ import tomllib from pathlib import Path from bulk_organizer.scanner import scanner +from bulk_organizer.mapper import map_extension_to_folder def get_version(): @@ -14,10 +15,14 @@ def get_version(): def main(): parser = argparse.ArgumentParser() + organized = {} + parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}") parser.add_argument("directory", type=str, help="Path to the directory to organize") parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes") parser.add_argument("--recursive", action="store_true", default=True, help="Recursively scan subdirectories") + parser.add_argument("--summary", action="store_true", help="Show compact summary instead of full paths") + args = parser.parse_args() directory_path = Path(args.directory) @@ -30,7 +35,22 @@ def main(): # Scan and print file paths try: for file_path in scanner(directory_path, recursive=args.recursive): - print(file_path) + folder = map_extension_to_folder(file_path) + + target_path = Path(args.directory) / folder / file_path.name + + if args.summary: + organized.setdefault(folder, []).append(file_path) + else: + print(f"Move: {file_path} โ†’ {target_path}") + + if args.summary: + print("\nDry run plan:") + for folder, files in organized.items(): + print(f"\n๐Ÿ“ {folder} ({len(files)} files):") + for f in files: + print(f" - {f.name}") + except ValueError as e: print(f"Error: {e}") diff --git a/bulk-file-organizer/bulk_organizer/mapper.py b/bulk-file-organizer/bulk_organizer/mapper.py index e69de29..91e15a8 100644 --- a/bulk-file-organizer/bulk_organizer/mapper.py +++ b/bulk-file-organizer/bulk_organizer/mapper.py @@ -0,0 +1,27 @@ +from pathlib import Path +from bulk_organizer.utils import normalize_extension + + +DEFAULT_MAP = { + "Images": [".jpg", ".jpeg", ".png", ".gif"], + "Documents": [".pdf", ".docx", ".txt"], + "Archives": [".zip", ".tar", ".gz"], + "Audio": [".mp3", ".wav"], +} + +def map_extension_to_folder(file_path: Path, mapping: dict = DEFAULT_MAP) -> str: + """Map a file extension to its corresponding folder name. + + Args: + file_path (Path): The file path to map. + mapping (dict): A dictionary mapping folder names to lists of extensions. + + Returns: + str: The folder name corresponding to the file's extension, or "Others" if not found. + """ + extension = normalize_extension(file_path.suffix) + + for folder , extensions in mapping.items(): + if extension in extensions: + return folder + return "Others" \ No newline at end of file diff --git a/bulk-file-organizer/bulk_organizer/actions.py b/bulk-file-organizer/bulk_organizer/organizer.py similarity index 100% rename from bulk-file-organizer/bulk_organizer/actions.py rename to bulk-file-organizer/bulk_organizer/organizer.py diff --git a/bulk-file-organizer/bulk_organizer/utils.py b/bulk-file-organizer/bulk_organizer/utils.py index e69de29..1a1cb73 100644 --- a/bulk-file-organizer/bulk_organizer/utils.py +++ b/bulk-file-organizer/bulk_organizer/utils.py @@ -0,0 +1,2 @@ +def normalize_extension(ext: str) -> str: + return ext.lower().strip() diff --git a/bulk-file-organizer/tests/test_mapper.py b/bulk-file-organizer/tests/test_mapper.py index 1cf4358..f3df069 100644 --- a/bulk-file-organizer/tests/test_mapper.py +++ b/bulk-file-organizer/tests/test_mapper.py @@ -1,6 +1,7 @@ import pytest from pathlib import Path from bulk_organizer.scanner import scanner +from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP def test_scanner_non_recursive(tmp_path): # Setup: create files and folders @@ -24,3 +25,12 @@ def test_scanner_recursive(tmp_path): assert len(files) == 2 assert sorted([f.name for f in files]) == ["file1.txt", "file2.txt"] + + +def test_known_extension(): + assert map_extension_to_folder(Path("photo.JPG"), DEFAULT_MAP) == "Images" + assert map_extension_to_folder(Path("song.wav"), DEFAULT_MAP) == "Audio" + +def test_unknown_extension(): + assert map_extension_to_folder(Path("weird.xyz"), DEFAULT_MAP) == "Others" + From 52dbca7d1dfd5a14ee5a31a622368d5a812f9d61 Mon Sep 17 00:00:00 2001 From: Shravan250 Date: Fri, 17 Oct 2025 00:58:57 +0530 Subject: [PATCH 4/5] enhance CLI and organizer functionality with dry-run simulation and summary output --- bulk-file-organizer/bulk_organizer/cli.py | 59 ++++++++++++++----- .../bulk_organizer/organizer.py | 38 ++++++++++++ bulk-file-organizer/tests/test_mapper.py | 14 +++++ 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/bulk-file-organizer/bulk_organizer/cli.py b/bulk-file-organizer/bulk_organizer/cli.py index 4de21b9..afb250f 100644 --- a/bulk-file-organizer/bulk_organizer/cli.py +++ b/bulk-file-organizer/bulk_organizer/cli.py @@ -1,9 +1,14 @@ import argparse import tomllib from pathlib import Path +from rich.console import Console +from rich.table import Table +from rich.progress import track from bulk_organizer.scanner import scanner -from bulk_organizer.mapper import map_extension_to_folder +from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP +from bulk_organizer.organizer import organize_file +console = Console() def get_version(): toml_path = Path(__file__).resolve().parent.parent / "pyproject.toml" @@ -16,43 +21,65 @@ def main(): parser = argparse.ArgumentParser() organized = {} + CATEGORIZED_FOLDERS = set(DEFAULT_MAP.keys()) | {"Others"} parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}") parser.add_argument("directory", type=str, help="Path to the directory to organize") parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes") parser.add_argument("--recursive", action="store_true", default=True, help="Recursively scan subdirectories") - parser.add_argument("--summary", action="store_true", help="Show compact summary instead of full paths") + parser.add_argument("--summary", action="store_true",default=True, help="Show compact summary instead of full paths") args = parser.parse_args() directory_path = Path(args.directory) + console.rule(f"[bold cyan]Bulk Organizer v{get_version()}[/bold cyan]") + console.print(f"Target Directory: [bold yellow]{directory_path}[/bold yellow]\n") + - print(f"Organizing: {directory_path}") if args.dry_run: - print("Dry run mode enabled") + console.print("[bold magenta]Dry Run Mode Enabled[/bold magenta] โ€” no files will be moved.\n") - # Scan and print file paths try: - for file_path in scanner(directory_path, recursive=args.recursive): + files = list(scanner(directory_path, recursive=args.recursive)) + first = True + for file_path in track(files, description="โœจ Organizing files..."): + if first: + console.print() + console.print() + first = False + folder = map_extension_to_folder(file_path) - target_path = Path(args.directory) / folder / file_path.name + # Skip already-organized files + if file_path.parent.name in CATEGORIZED_FOLDERS: + continue + + # Always track for summary + organized.setdefault(folder, []).append(file_path) - if args.summary: - organized.setdefault(folder, []).append(file_path) - else: - print(f"Move: {file_path} โ†’ {target_path}") + # Perform move or simulate + organize_file(file_path, directory_path, folder, dry_run=args.dry_run) + + console.print() if args.summary: - print("\nDry run plan:") + console.print("\n๐Ÿ“Š [bold underline]Organization Summary[/bold underline]\n") + table = Table(show_header=True, header_style="bold blue") + table.add_column("Folder", style="cyan") + table.add_column("File Count", style="green") + table.add_column("Example Files", style="yellow") + for folder, files in organized.items(): - print(f"\n๐Ÿ“ {folder} ({len(files)} files):") - for f in files: - print(f" - {f.name}") + examples = ", ".join(f.name for f in files[:3]) + table.add_row(folder, str(len(files)), examples + (" ..." if len(files) > 3 else "")) + + console.print(table) + + console.rule("[bold green]Operation Complete[/bold green]") except ValueError as e: - print(f"Error: {e}") + console.print(f"[bold red]Error:[/bold red] {e}") if __name__ == "__main__": main() diff --git a/bulk-file-organizer/bulk_organizer/organizer.py b/bulk-file-organizer/bulk_organizer/organizer.py index e69de29..1f413c0 100644 --- a/bulk-file-organizer/bulk_organizer/organizer.py +++ b/bulk-file-organizer/bulk_organizer/organizer.py @@ -0,0 +1,38 @@ +import shutil +from pathlib import Path +from rich.console import Console + +console = Console() + +def organize_file(file_path: Path, base_dir: Path, folder_name: str, dry_run: bool = False): + """Move a file into its categorized folder, handling collisions and dry-run mode. + + Args: + file_path (Path): The original file path. + base_dir (Path): The base directory where folders are created. + folder_name (str): The target folder name. + dry_run (bool): If True, simulate the move without performing it. + """ + target_dir = base_dir / folder_name + target_dir.mkdir(parents=True, exist_ok=True) + target_path = target_dir / file_path.name + + # handle name collisions + counter = 1 + while target_path.exists(): + target_path = target_dir / f"{file_path.stem} {counter}{file_path.suffix}" + counter += 1 + + if dry_run: + console.print( + f"[dim yellow][Dry Run][/dim yellow] {file_path.relative_to(base_dir)} โ†’ " + f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]" + ) + else: + shutil.move(str(file_path), str(target_path)) + console.print( + f"[green]โœ” Moved[/green] [bold white]{file_path.relative_to(base_dir)}[/bold white] โ†’ " + f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]" + ) + + diff --git a/bulk-file-organizer/tests/test_mapper.py b/bulk-file-organizer/tests/test_mapper.py index f3df069..e17b908 100644 --- a/bulk-file-organizer/tests/test_mapper.py +++ b/bulk-file-organizer/tests/test_mapper.py @@ -2,6 +2,7 @@ from pathlib import Path from bulk_organizer.scanner import scanner from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP +from bulk_organizer.organizer import organize_file def test_scanner_non_recursive(tmp_path): # Setup: create files and folders @@ -34,3 +35,16 @@ def test_known_extension(): def test_unknown_extension(): assert map_extension_to_folder(Path("weird.xyz"), DEFAULT_MAP) == "Others" + +def test_organize_file_dry_run(tmp_path, capsys): + file = tmp_path / "test.txt" + file.write_text("hello") + + organize_file(file, tmp_path, "Documents", dry_run=True) + + # File should still exist in original location + assert file.exists() + + # Check printed output + captured = capsys.readouterr() + assert "[Dry Run]" in captured.out \ No newline at end of file From 22e39ea9c00cd0b0fd873a135cd7616f928ec57a Mon Sep 17 00:00:00 2001 From: Shravan250 Date: Fri, 17 Oct 2025 11:38:43 +0530 Subject: [PATCH 5/5] updated ReadMe --- bulk-file-organizer/CONTRIBUTING.md | 0 bulk-file-organizer/README.md | 124 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) delete mode 100644 bulk-file-organizer/CONTRIBUTING.md diff --git a/bulk-file-organizer/CONTRIBUTING.md b/bulk-file-organizer/CONTRIBUTING.md deleted file mode 100644 index e69de29..0000000 diff --git a/bulk-file-organizer/README.md b/bulk-file-organizer/README.md index e69de29..3db3a37 100644 --- a/bulk-file-organizer/README.md +++ b/bulk-file-organizer/README.md @@ -0,0 +1,124 @@ +# ๐Ÿ—‚๏ธ Bulk File Organizer โ€” Integration Module + +**Bulk File Organizer** is a Python command-line tool that automatically sorts files in a directory based on their extensions. +It transforms digital clutter into clean, structured folders โ€” with vibrant terminal feedback powered by **Rich** โœจ + +--- + +## ๐Ÿ“ฃ Latest Update: Integrated Module + +This release brings integration with the [Bulk File Organizer](https://github.com/Shravan250/bulk-file-organizer) โ€” a CLI utility by [Shravan Bobade](https://github.com/Shravan250) that organizes files by type, supports dry-run simulations, and provides colorful output using the `rich` library. + +### โœจ Highlights + +- ๐Ÿ“ฆ Automatically sorts files into categorized folders (Images, Documents, Archives, etc.) +- ๐Ÿ” Recursive directory scanning +- ๐Ÿงช Dry-run simulation mode (no files moved) +- ๐ŸŒˆ Beautiful progress bars & summary tables +- โš™๏ธ Extensible via custom mappings + +--- + +## โš™๏ธ Installation + +```bash +pip install bulk-file-organizer +``` + +Or install directly from source: + +```bash +git clone https://github.com/Shravan250/bulk-file-organizer.git +cd bulk-file-organizer +pip install . +``` + +--- + +## ๐Ÿ–ฅ๏ธ Usage + +```bash +bulk-organizer /path/to/your/folder +``` + +### Options + +| Flag | Description | +| ------------- | ------------------------------------------ | +| `--dry-run` | Simulate organization without moving files | +| `--summary` | Display a summary table after organization | +| `--recursive` | Recursively organize subdirectories | +| `--version` | Display the current version | + +#### Example + +```bash +bulk-organizer ~/Downloads --dry-run --summary +``` + +--- + +## ๐Ÿง  Example Output + +```text +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Bulk Organizer v0.1.0 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +Target Directory: ~/Downloads + +Dry Run Mode Enabled โ€” no files will be moved. + +โœจ Organizing files... โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 100% 0:00:02 + +๐Ÿ“Š Organization Summary + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Folder โ”ƒ File Count โ”ƒ Example Files โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ Images โ”‚ 12 โ”‚ photo.png, logo.jpg, โ€ฆ โ”‚ +โ”‚ Documents โ”‚ 8 โ”‚ resume.pdf, notes.txt, โ€ฆ โ”‚ +โ”‚ Archives โ”‚ 2 โ”‚ backup.zip, logs.tar.gz โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Operation Complete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +``` + +--- + +## ๐Ÿงฉ Project Structure + +``` +bulk-file-organizer/ +โ”‚ +โ”œโ”€โ”€ bulk_organizer/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ cli.py +โ”‚ โ”œโ”€โ”€ scanner.py +โ”‚ โ”œโ”€โ”€ mapper.py +โ”‚ โ”œโ”€โ”€ organizer.py +โ”‚ โ””โ”€โ”€ utils.py +โ”‚ +โ”œโ”€โ”€ tests/ +โ”œโ”€โ”€ pyproject.toml +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ CONTRIBUTING.md +โ””โ”€โ”€ LICENSE +``` + +--- + +## ๐Ÿค Contributing + +We love clean code and clever minds. +Check out the [CONTRIBUTING.md](https://github.com/Shravan250/bulk-file-organizer/blob/main/CONTRIBUTING.md) to learn how you can contribute and make this project even better. + +--- + +## ๐Ÿชช License + +Licensed under the **MIT License**. + +--- + +## โญ Show Some Love + +If this project helped you bring order to your chaos โ€” +please consider giving it a โญ on [GitHub](https://github.com/Shravan250/bulk-file-organizer)! +Every star helps this project grow and keeps the world just a little more organized ๐ŸŒโœจ