From 309ef89d6ff8f8681bfa3b5f015714ff356eba47 Mon Sep 17 00:00:00 2001 From: aga Date: Thu, 26 Mar 2026 16:33:41 +0100 Subject: [PATCH 1/2] Rename command to separate-raws and keep non-RAW files in place --- README.md | 9 ++++---- src/photo_tools/cli.py | 17 ++++++++------ .../{organise_by_type.py => separate_raws.py} | 22 ++++++------------- 3 files changed, 22 insertions(+), 26 deletions(-) rename src/photo_tools/{organise_by_type.py => separate_raws.py} (54%) diff --git a/README.md b/README.md index 4d1f3c2..c9e62bd 100644 --- a/README.md +++ b/README.md @@ -90,17 +90,18 @@ photo-tools organise-by-date --suffix photo-tools organise-by-date --dry-run ``` -#### organise-by-type +#### separate-raws -- Organise images into `raws/` and `jpgs/` subfolders within the input directory +- Move RAW images into a `raws/` subfolder within the input directory +- Non-RAW files are left unchanged - Files are moved (not copied) in place - If a destination file already exists, it is skipped (no overwrite) ```shell -photo-tools organise-by-type +photo-tools separate-raws ``` ```shell -photo-tools organise-by-type --dry-run +photo-tools separate-raws --dry-run ``` #### soft-delete-unpaired-raws diff --git a/src/photo_tools/cli.py b/src/photo_tools/cli.py index c94f4c5..4516909 100644 --- a/src/photo_tools/cli.py +++ b/src/photo_tools/cli.py @@ -5,7 +5,7 @@ from photo_tools.logging_config import setup_logging from photo_tools.optimise import optimise from photo_tools.organise_by_date import organise_by_date -from photo_tools.organise_by_type import organise_by_type +from photo_tools.separate_raws import separate_raws from photo_tools.soft_delete_unpaired_raws import soft_delete_unpaired_raws app = typer.Typer(help="CLI tools for organising and optimising photography workflows.") @@ -49,20 +49,23 @@ def organise_by_date_cmd( organise_by_date(input_dir, output_dir, suffix, dry_run) -@app.command("organise-by-type") -def organise_by_type_cmd( +@app.command( + "separate-raws", + help="Move RAW images into a 'raws' folder", +) +def separate_raws_cmd( input_dir: str = typer.Argument( ..., - help="Directory containing images to organise by type (JPG and RAF supported)", + help="Directory containing images from which RAW files should be separated.", ), dry_run: bool = typer.Option( False, "--dry-run", - help="Preview changes without moving files", + help="Preview changes without moving files.", ), ) -> None: - """Organise images into 'jpgs' and 'raws' folders (supports .jpg/.jpeg and .raf).""" - organise_by_type(input_dir, dry_run) + """Move RAW images into a 'raws' folder.""" + separate_raws(input_dir, dry_run) @app.command( diff --git a/src/photo_tools/organise_by_type.py b/src/photo_tools/separate_raws.py similarity index 54% rename from src/photo_tools/organise_by_type.py rename to src/photo_tools/separate_raws.py index a6cf886..1c74a48 100644 --- a/src/photo_tools/organise_by_type.py +++ b/src/photo_tools/separate_raws.py @@ -7,41 +7,33 @@ logger = logging.getLogger(__name__) RAW_EXTENSIONS = {".raf"} -JPG_EXTENSIONS = {".jpg", ".jpeg"} -def organise_by_type(input_dir: str, dry_run: bool = False) -> None: +def separate_raws(input_dir: str, dry_run: bool = False) -> None: input_path = Path(input_dir) validate_input_dir(input_path) raws_dir = input_path / "raws" - jpgs_dir = input_path / "jpgs" for file_path in input_path.iterdir(): if not file_path.is_file(): continue - ext = file_path.suffix.lower() - - if ext in RAW_EXTENSIONS: - target_dir = raws_dir - elif ext in JPG_EXTENSIONS: - target_dir = jpgs_dir - else: - logger.debug(f"Skipping (unsupported): {file_path.name}") + if file_path.suffix.lower() not in RAW_EXTENSIONS: + logger.debug(f"Skipping (not RAW): {file_path.name}") continue - target_file = target_dir / file_path.name + target_file = raws_dir / file_path.name if target_file.exists(): logger.info(f"Skipping (already exists): {target_file.name}") continue if dry_run: - logger.info(f"[DRY RUN] Would move {file_path.name} → {target_dir}") + logger.info(f"[DRY RUN] Would move {file_path.name} → {raws_dir}") continue - target_dir.mkdir(parents=True, exist_ok=True) + raws_dir.mkdir(parents=True, exist_ok=True) shutil.move(str(file_path), str(target_file)) - logger.info(f"Moved {file_path.name} → {target_dir}") + logger.info(f"Moved {file_path.name} → {raws_dir}") \ No newline at end of file From 4c27523e3c80023e6f3902cdc528041678d27e19 Mon Sep 17 00:00:00 2001 From: aga Date: Thu, 26 Mar 2026 16:38:50 +0100 Subject: [PATCH 2/2] Add tests for separate_raws command behaviour --- README.md | 9 --- src/photo_tools/separate_raws.py | 2 +- tests/test_separate_raws.py | 109 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 tests/test_separate_raws.py diff --git a/README.md b/README.md index c9e62bd..7386a34 100644 --- a/README.md +++ b/README.md @@ -165,15 +165,6 @@ This project uses `pytest`: pytest ``` -### Test coverage - -The testing pattern is fully implemented for organise-by-date, including: -- dry-run safety -- file movement -- edge cases (unsupported files, missing metadata, collisions) - -Other commands follow the same structure but are not yet covered. - ### Makefile Common development tasks are available via the Makefile. diff --git a/src/photo_tools/separate_raws.py b/src/photo_tools/separate_raws.py index 1c74a48..27d4684 100644 --- a/src/photo_tools/separate_raws.py +++ b/src/photo_tools/separate_raws.py @@ -36,4 +36,4 @@ def separate_raws(input_dir: str, dry_run: bool = False) -> None: raws_dir.mkdir(parents=True, exist_ok=True) shutil.move(str(file_path), str(target_file)) - logger.info(f"Moved {file_path.name} → {raws_dir}") \ No newline at end of file + logger.info(f"Moved {file_path.name} → {raws_dir}") diff --git a/tests/test_separate_raws.py b/tests/test_separate_raws.py new file mode 100644 index 0000000..68f0a94 --- /dev/null +++ b/tests/test_separate_raws.py @@ -0,0 +1,109 @@ +from photo_tools.separate_raws import separate_raws + + +def test_dry_run_does_not_move_raw_files(tmp_path): + input_dir = tmp_path / "input" + input_dir.mkdir() + + raw_file = input_dir / "photo.raf" + raw_file.write_text("fake raw content") + + separate_raws(str(input_dir), dry_run=True) + + assert raw_file.exists() + assert not (input_dir / "raws" / "photo.raf").exists() + + +def test_moves_raw_file_into_raws_folder(tmp_path): + input_dir = tmp_path / "input" + input_dir.mkdir() + + raw_file = input_dir / "photo.raf" + raw_file.write_text("fake raw content") + + separate_raws(str(input_dir), dry_run=False) + + moved_file = input_dir / "raws" / "photo.raf" + + assert not raw_file.exists() + assert moved_file.exists() + + +def test_leaves_non_raw_files_in_place(tmp_path): + input_dir = tmp_path / "input" + input_dir.mkdir() + + jpg_file = input_dir / "photo.jpg" + txt_file = input_dir / "notes.txt" + + jpg_file.write_text("fake jpg content") + txt_file.write_text("notes") + + separate_raws(str(input_dir), dry_run=False) + + assert jpg_file.exists() + assert txt_file.exists() + assert not (input_dir / "raws" / "photo.jpg").exists() + assert not (input_dir / "raws" / "notes.txt").exists() + + +def test_moves_only_raw_files(tmp_path): + input_dir = tmp_path / "input" + input_dir.mkdir() + + raw_file = input_dir / "photo.raf" + jpg_file = input_dir / "photo.jpg" + + raw_file.write_text("fake raw content") + jpg_file.write_text("fake jpg content") + + separate_raws(str(input_dir), dry_run=False) + + assert (input_dir / "raws" / "photo.raf").exists() + assert jpg_file.exists() + assert not raw_file.exists() + + +def test_skips_file_when_destination_already_exists(tmp_path): + input_dir = tmp_path / "input" + raws_dir = input_dir / "raws" + + input_dir.mkdir() + raws_dir.mkdir() + + source_file = input_dir / "photo.raf" + existing_file = raws_dir / "photo.raf" + + source_file.write_text("new raw") + existing_file.write_text("existing raw") + + separate_raws(str(input_dir), dry_run=False) + + assert source_file.exists() + assert existing_file.exists() + assert existing_file.read_text() == "existing raw" + + +def test_handles_empty_input_directory(tmp_path): + input_dir = tmp_path / "input" + input_dir.mkdir() + + separate_raws(str(input_dir), dry_run=False) + + assert not any(input_dir.iterdir()) + + +def test_ignores_nested_directories(tmp_path): + input_dir = tmp_path / "input" + nested_dir = input_dir / "nested" + + input_dir.mkdir() + nested_dir.mkdir() + + nested_raw = nested_dir / "photo.raf" + nested_raw.write_text("fake raw content") + + separate_raws(str(input_dir), dry_run=False) + + assert nested_raw.exists() + assert not (input_dir / "raws" / "photo.raf").exists()