From 597be2691c4b3e58cfd34a02f2c64250bb17767a Mon Sep 17 00:00:00 2001 From: zoltan57 <40281233+zoltan57@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:00:02 -0500 Subject: [PATCH] V4.6 Phase 6 follow-up: resolve homepage storage from settings ui/homepage_store.py was the only storage path in the codebase derived from Path(__file__).parents[3] rather than from Settings. That made it the one storage root the operator could not relocate, and it resolved incorrectly outside a source checkout - an installed distribution would have written homepage content into the package directory in site-packages. - config.py: add homepage_dir, defaulting to ./data/homepage so the location is unchanged for anyone launching from the repository root. - homepage_store.py: resolve the directory and markdown path from Settings, with an optional settings parameter on every function so callers and tests can override without patching module constants. HOME_PAGE_DIR and HOME_PAGE_MARKDOWN_PATH constants are replaced by homepage_dir() and homepage_markdown_path(). - tests/ui/test_homepage_store.py: covers the setting being honored, markdown round-tripping, image storage and listing, and two configurations not sharing storage. Note: the default is now CWD-relative, matching artifact_dir and upload_dir, rather than anchored to the repository root. Verification: ruff check src tests clean; 292 passed, 4 skipped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/transcription/config.py | 1 + src/transcription/ui/homepage_store.py | 68 +++++++++++++++++--------- tests/ui/test_homepage_store.py | 65 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 tests/ui/test_homepage_store.py diff --git a/src/transcription/config.py b/src/transcription/config.py index 3cb81b1..df42275 100644 --- a/src/transcription/config.py +++ b/src/transcription/config.py @@ -102,6 +102,7 @@ class Settings(BaseSettings): upload_dir: Path = Path("./uploads") prompt_dir: Path = Path("./prompts") artifact_dir: Path = Path("./data/artifacts") + homepage_dir: Path = Path("./data/homepage") artifact_inline_threshold_bytes: int = Field(default=1_048_576, ge=1) # --- worker reliability --- diff --git a/src/transcription/ui/homepage_store.py b/src/transcription/ui/homepage_store.py index 7e3b3c5..517b6b8 100644 --- a/src/transcription/ui/homepage_store.py +++ b/src/transcription/ui/homepage_store.py @@ -1,14 +1,22 @@ -"""File-backed storage helpers for the homepage content.""" +"""File-backed storage helpers for the homepage content. + +The homepage storage root is a configured setting (``homepage_dir``) like every +other storage root, rather than a path derived from this module's location. The +previous ``Path(__file__).parents[3]`` form was both unconfigurable and wrong +outside a source checkout, since an installed distribution would resolve it into +the package directory. +""" from __future__ import annotations from pathlib import Path +from transcription.config import Settings +from transcription.config import get_settings from transcription.errors import AppError from transcription.services.media_storage import write_media_bytes -HOME_PAGE_DIR = Path(__file__).resolve().parents[3] / "data" / "homepage" -HOME_PAGE_MARKDOWN_PATH = HOME_PAGE_DIR / "homepage.md" +HOME_PAGE_MARKDOWN_NAME = "homepage.md" SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"} @@ -16,26 +24,44 @@ class HomepageStorageError(AppError): """Raised when homepage media cannot be persisted.""" -def ensure_homepage_storage() -> None: - """Create the homepage storage directory when needed.""" - HOME_PAGE_DIR.mkdir(parents=True, exist_ok=True) +def homepage_dir(settings: Settings | None = None) -> Path: + """Return the configured homepage storage directory.""" + return (settings or get_settings()).homepage_dir -def read_homepage_markdown() -> str: +def homepage_markdown_path(settings: Settings | None = None) -> Path: + """Return the configured homepage markdown file path.""" + return homepage_dir(settings) / HOME_PAGE_MARKDOWN_NAME + + +def ensure_homepage_storage(settings: Settings | None = None) -> Path: + """Create the homepage storage directory when needed and return it.""" + directory = homepage_dir(settings) + directory.mkdir(parents=True, exist_ok=True) + return directory + + +def read_homepage_markdown(settings: Settings | None = None) -> str: """Read the saved homepage markdown text.""" - ensure_homepage_storage() - if not HOME_PAGE_MARKDOWN_PATH.exists(): + ensure_homepage_storage(settings) + path = homepage_markdown_path(settings) + if not path.exists(): return "" - return HOME_PAGE_MARKDOWN_PATH.read_text(encoding="utf-8") + return path.read_text(encoding="utf-8") -def save_homepage_markdown(markdown_text: str) -> None: +def save_homepage_markdown(markdown_text: str, settings: Settings | None = None) -> None: """Persist the homepage markdown text.""" - ensure_homepage_storage() - HOME_PAGE_MARKDOWN_PATH.write_text(markdown_text, encoding="utf-8") + ensure_homepage_storage(settings) + homepage_markdown_path(settings).write_text(markdown_text, encoding="utf-8") -async def store_homepage_image(*, filename: str, file_bytes: bytes) -> Path: +async def store_homepage_image( + *, + filename: str, + file_bytes: bytes, + settings: Settings | None = None, +) -> Path: """Persist an uploaded homepage image in the shared homepage folder.""" safe_name = Path(filename).name if not safe_name: @@ -43,7 +69,7 @@ async def store_homepage_image(*, filename: str, file_bytes: bytes) -> Path: raise ValueError(msg) return await write_media_bytes( - target_dir=HOME_PAGE_DIR, + target_dir=homepage_dir(settings), stored_name=safe_name, file_bytes=file_bytes, error=HomepageStorageError, @@ -53,21 +79,19 @@ async def store_homepage_image(*, filename: str, file_bytes: bytes) -> Path: ) -def list_homepage_images() -> list[Path]: +def list_homepage_images(settings: Settings | None = None) -> list[Path]: """List stored homepage images in the order they were last updated.""" - ensure_homepage_storage() + directory = ensure_homepage_storage(settings) image_paths = [ - path - for path in HOME_PAGE_DIR.iterdir() - if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_SUFFIXES + path for path in directory.iterdir() if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_SUFFIXES ] return sorted(image_paths, key=lambda path: (path.stat().st_mtime, path.name)) -def latest_homepage_image() -> Path | None: +def latest_homepage_image(settings: Settings | None = None) -> Path | None: """Return the most recently updated homepage image, if one exists.""" - image_paths = list_homepage_images() + image_paths = list_homepage_images(settings) if not image_paths: return None return image_paths[-1] diff --git a/tests/ui/test_homepage_store.py b/tests/ui/test_homepage_store.py new file mode 100644 index 0000000..9560467 --- /dev/null +++ b/tests/ui/test_homepage_store.py @@ -0,0 +1,65 @@ +"""Homepage storage resolves its root from settings rather than from `__file__`. + +The previous module derived its directory from ``Path(__file__).parents[3]``, +which could not be configured and resolved into the installed package directory +outside a source checkout. +""" + +import pytest + +from transcription.config import Settings +from transcription.ui.homepage_store import homepage_dir +from transcription.ui.homepage_store import latest_homepage_image +from transcription.ui.homepage_store import list_homepage_images +from transcription.ui.homepage_store import read_homepage_markdown +from transcription.ui.homepage_store import save_homepage_markdown +from transcription.ui.homepage_store import store_homepage_image + +PNG_BYTES = bytes.fromhex( + "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000a49444154789c6360000002000100" + "05fe02fea7b1b8000000004945" +) + b"NDAE\xae\x42\x60\x82" + + +def _settings(tmp_path) -> Settings: + return Settings(openrouter_api_key="test-key-abc123", homepage_dir=tmp_path / "homepage") + + +def test_homepage_dir_follows_the_configured_setting(tmp_path): + settings = _settings(tmp_path) + + assert homepage_dir(settings) == tmp_path / "homepage" + + +def test_markdown_round_trips_through_the_configured_directory(tmp_path): + settings = _settings(tmp_path) + + assert read_homepage_markdown(settings) == "" + + save_homepage_markdown("# Archive", settings) + + assert (tmp_path / "homepage" / "homepage.md").read_text(encoding="utf-8") == "# Archive" + assert read_homepage_markdown(settings) == "# Archive" + + +@pytest.mark.asyncio +async def test_images_are_stored_and_listed_from_the_configured_directory(tmp_path): + settings = _settings(tmp_path) + + assert list_homepage_images(settings) == [] + assert latest_homepage_image(settings) is None + + stored = await store_homepage_image(filename="banner.png", file_bytes=PNG_BYTES, settings=settings) + + assert stored.parent == tmp_path / "homepage" + assert list_homepage_images(settings) == [stored] + assert latest_homepage_image(settings) == stored + + +def test_two_configurations_do_not_share_storage(tmp_path): + first = Settings(openrouter_api_key="test-key-abc123", homepage_dir=tmp_path / "a") + second = Settings(openrouter_api_key="test-key-abc123", homepage_dir=tmp_path / "b") + + save_homepage_markdown("first", first) + + assert read_homepage_markdown(second) == ""