refactor: rename project to winnow

- Rename Python package directory if_curator/ → winnow/
- Update all imports, entry points, and CLI references
- Update pyproject.toml: name, scripts, package list, repository URL
- Update Dockerfile, compose.yml, entrypoint.sh, scheduler.py
- Update GitHub Actions workflow image names
- Update README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:07:46 +00:00
co-authored by Claude Sonnet 4.6
parent ca7b90f8ef
commit eea7baa19f
28 changed files with 126 additions and 118 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ def test_config_loads_defaults(monkeypatch):
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
monkeypatch.setenv("API_KEY", "test-key")
from if_curator.config import _Config
from winnow.config import _Config
_Config.reset()
cfg = _Config()
@@ -46,7 +46,7 @@ def test_config_env_overrides(monkeypatch):
monkeypatch.setenv("ENABLE_FACE_ALIGNMENT", "false")
monkeypatch.setenv("ENABLE_CACHE", "true")
from if_curator.config import _Config
from winnow.config import _Config
_Config.reset()
cfg = _Config()
@@ -69,7 +69,7 @@ def test_get_headers_returns_api_key(monkeypatch):
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
monkeypatch.setenv("API_KEY", "my-secret-key")
from if_curator.config import _Config, get_headers
from winnow.config import _Config, get_headers
_Config.reset()
headers = get_headers()
+6 -6
View File
@@ -9,14 +9,14 @@ def _asset(days_ago: int) -> dict:
def test_filter_recent_keeps_new_assets(monkeypatch):
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
assets = [_asset(1), _asset(30), _asset(365 * 5)]
result = filter_recent_assets(assets, years=10)
assert len(result) == 3
def test_filter_recent_removes_old_assets(monkeypatch):
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
old = _asset(365 * 15)
recent = _asset(10)
result = filter_recent_assets([old, recent], years=10)
@@ -25,7 +25,7 @@ def test_filter_recent_removes_old_assets(monkeypatch):
def test_filter_recent_boundary(monkeypatch):
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
just_inside = _asset(365 * 10 - 1)
just_outside = _asset(365 * 10 + 1)
result = filter_recent_assets([just_inside, just_outside], years=10)
@@ -34,7 +34,7 @@ def test_filter_recent_boundary(monkeypatch):
def test_filter_recent_skips_missing_date():
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
assets = [{"id": "no-date"}, _asset(5)]
result = filter_recent_assets(assets, years=10)
assert len(result) == 1
@@ -42,12 +42,12 @@ def test_filter_recent_skips_missing_date():
def test_filter_recent_skips_bad_date():
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
assets = [{"id": "bad", "fileCreatedAt": "not-a-date"}, _asset(5)]
result = filter_recent_assets(assets, years=10)
assert len(result) == 1
def test_filter_recent_empty_list():
from if_curator.immich_api import filter_recent_assets
from winnow.immich_api import filter_recent_assets
assert filter_recent_assets([], years=10) == []
+8 -8
View File
@@ -11,21 +11,21 @@ def clean_env(monkeypatch):
def test_resolve_strategy_default_auto():
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("auto", has_embedding=True)
assert limit == "auto"
assert mode == "smart"
def test_resolve_strategy_standard():
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("standard", has_embedding=True)
assert limit == 30
assert mode == "smart"
def test_resolve_strategy_broad():
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("broad", has_embedding=True)
assert limit == 100
assert mode == "smart"
@@ -33,7 +33,7 @@ def test_resolve_strategy_broad():
def test_resolve_strategy_custom_limit_env(monkeypatch):
monkeypatch.setenv("LIMIT", "50")
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("auto", has_embedding=True)
assert limit == 50
assert mode == "smart"
@@ -41,13 +41,13 @@ def test_resolve_strategy_custom_limit_env(monkeypatch):
def test_resolve_strategy_limit_overrides_strategy(monkeypatch):
monkeypatch.setenv("LIMIT", "25")
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("broad", has_embedding=True)
assert limit == 25
def test_resolve_strategy_no_embedding_falls_back_to_time():
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("auto", has_embedding=False)
assert mode == "time"
assert isinstance(limit, int)
@@ -55,14 +55,14 @@ def test_resolve_strategy_no_embedding_falls_back_to_time():
def test_resolve_strategy_no_embedding_respects_limit(monkeypatch):
monkeypatch.setenv("LIMIT", "60")
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("auto", has_embedding=False)
assert limit == 60
assert mode == "time"
def test_resolve_strategy_unknown_falls_back_to_auto():
from if_curator.jobs import _resolve_strategy
from winnow.jobs import _resolve_strategy
limit, mode = _resolve_strategy("unknown-strategy", has_embedding=True)
assert limit == "auto"
assert mode == "smart"
+8 -8
View File
@@ -11,41 +11,41 @@ import pytest
def isolated_cache(monkeypatch, tmp_path):
"""Point tracker at a temp directory so tests don't touch real cache files."""
monkeypatch.setenv("CACHE_DIR", str(tmp_path))
from if_curator.config import _Config
from winnow.config import _Config
_Config.reset()
yield tmp_path
_Config.reset()
def test_filter_returns_all_when_empty():
from if_curator.upload_tracker import filter_already_uploaded
from winnow.upload_tracker import filter_already_uploaded
ids = ["a1", "b2", "c3"]
assert filter_already_uploaded(ids) == ids
def test_mark_uploaded_excludes_from_filter():
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded
mark_uploaded("a1", person_name="Alice")
result = filter_already_uploaded(["a1", "b2"])
assert result == ["b2"]
def test_mark_rejected_excludes_from_filter():
from if_curator.upload_tracker import filter_already_uploaded, mark_rejected
from winnow.upload_tracker import filter_already_uploaded, mark_rejected
mark_rejected("x9", person_name="Bob")
result = filter_already_uploaded(["x9", "y8"])
assert result == ["y8"]
def test_retry_rejected_includes_rejected():
from if_curator.upload_tracker import filter_already_uploaded, mark_rejected
from winnow.upload_tracker import filter_already_uploaded, mark_rejected
mark_rejected("x9", person_name="Bob")
result = filter_already_uploaded(["x9", "y8"], retry_rejected=True)
assert "x9" in result
def test_reset_person_clears_records():
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded, reset_person
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded, reset_person
mark_uploaded("a1", person_name="Alice")
mark_uploaded("a2", person_name="Alice")
mark_uploaded("b1", person_name="Bob")
@@ -55,7 +55,7 @@ def test_reset_person_clears_records():
def test_get_person_summary():
from if_curator.upload_tracker import get_person_summary, mark_rejected, mark_uploaded
from winnow.upload_tracker import get_person_summary, mark_rejected, mark_uploaded
mark_uploaded("a1", person_name="Alice")
mark_uploaded("a2", person_name="Alice")
mark_rejected("a3", person_name="Alice")
@@ -68,7 +68,7 @@ def test_get_person_summary():
def test_duplicate_marks_are_idempotent():
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded
mark_uploaded("dup", person_name="Alice")
mark_uploaded("dup", person_name="Alice")
assert filter_already_uploaded(["dup", "new"]) == ["new"]