generated from john/python-template
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 <[email protected]>
This commit is contained in:
@@ -102,6 +102,7 @@ class Settings(BaseSettings):
|
|||||||
upload_dir: Path = Path("./uploads")
|
upload_dir: Path = Path("./uploads")
|
||||||
prompt_dir: Path = Path("./prompts")
|
prompt_dir: Path = Path("./prompts")
|
||||||
artifact_dir: Path = Path("./data/artifacts")
|
artifact_dir: Path = Path("./data/artifacts")
|
||||||
|
homepage_dir: Path = Path("./data/homepage")
|
||||||
artifact_inline_threshold_bytes: int = Field(default=1_048_576, ge=1)
|
artifact_inline_threshold_bytes: int = Field(default=1_048_576, ge=1)
|
||||||
|
|
||||||
# --- worker reliability ---
|
# --- worker reliability ---
|
||||||
|
|||||||
@@ -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 __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from transcription.config import Settings
|
||||||
|
from transcription.config import get_settings
|
||||||
from transcription.errors import AppError
|
from transcription.errors import AppError
|
||||||
from transcription.services.media_storage import write_media_bytes
|
from transcription.services.media_storage import write_media_bytes
|
||||||
|
|
||||||
HOME_PAGE_DIR = Path(__file__).resolve().parents[3] / "data" / "homepage"
|
HOME_PAGE_MARKDOWN_NAME = "homepage.md"
|
||||||
HOME_PAGE_MARKDOWN_PATH = HOME_PAGE_DIR / "homepage.md"
|
|
||||||
SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
|
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."""
|
"""Raised when homepage media cannot be persisted."""
|
||||||
|
|
||||||
|
|
||||||
def ensure_homepage_storage() -> None:
|
def homepage_dir(settings: Settings | None = None) -> Path:
|
||||||
"""Create the homepage storage directory when needed."""
|
"""Return the configured homepage storage directory."""
|
||||||
HOME_PAGE_DIR.mkdir(parents=True, exist_ok=True)
|
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."""
|
"""Read the saved homepage markdown text."""
|
||||||
ensure_homepage_storage()
|
ensure_homepage_storage(settings)
|
||||||
if not HOME_PAGE_MARKDOWN_PATH.exists():
|
path = homepage_markdown_path(settings)
|
||||||
|
if not path.exists():
|
||||||
return ""
|
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."""
|
"""Persist the homepage markdown text."""
|
||||||
ensure_homepage_storage()
|
ensure_homepage_storage(settings)
|
||||||
HOME_PAGE_MARKDOWN_PATH.write_text(markdown_text, encoding="utf-8")
|
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."""
|
"""Persist an uploaded homepage image in the shared homepage folder."""
|
||||||
safe_name = Path(filename).name
|
safe_name = Path(filename).name
|
||||||
if not safe_name:
|
if not safe_name:
|
||||||
@@ -43,7 +69,7 @@ async def store_homepage_image(*, filename: str, file_bytes: bytes) -> Path:
|
|||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
return await write_media_bytes(
|
return await write_media_bytes(
|
||||||
target_dir=HOME_PAGE_DIR,
|
target_dir=homepage_dir(settings),
|
||||||
stored_name=safe_name,
|
stored_name=safe_name,
|
||||||
file_bytes=file_bytes,
|
file_bytes=file_bytes,
|
||||||
error=HomepageStorageError,
|
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."""
|
"""List stored homepage images in the order they were last updated."""
|
||||||
ensure_homepage_storage()
|
directory = ensure_homepage_storage(settings)
|
||||||
|
|
||||||
image_paths = [
|
image_paths = [
|
||||||
path
|
path for path in directory.iterdir() if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_SUFFIXES
|
||||||
for path in HOME_PAGE_DIR.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))
|
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."""
|
"""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:
|
if not image_paths:
|
||||||
return None
|
return None
|
||||||
return image_paths[-1]
|
return image_paths[-1]
|
||||||
|
|||||||
@@ -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) == ""
|
||||||
Reference in New Issue
Block a user