From 98e94beb1b24f6e8a28ee2f82740a586ff62c3f0 Mon Sep 17 00:00:00 2001 From: Wesley Simplicio Date: Sat, 9 May 2026 15:13:19 -0300 Subject: [PATCH] fix(deps): declare youtube-transcript-api in pyproject.toml [youtube] extra skills/media/youtube-content/scripts/fetch_transcript.py and optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py both import youtube-transcript-api at runtime, but the package was not listed in pyproject.toml. A fresh `uv sync` therefore omits it, and both skills fail on first invocation with: ModuleNotFoundError: No module named 'youtube_transcript_api' Add a new [youtube] optional-dependency group with youtube-transcript-api>=1.2.0 (the v1.x API surface the scripts already use) and include it in [all] so standard installs pick it up. Regression tests: TestPyprojectDeclaresYoutubeExtra verifies the extra is present in pyproject.toml and included in [all]. Closes #22243 Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 8 +++ tests/skills/test_fetch_transcript.py | 87 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/skills/test_fetch_transcript.py diff --git a/pyproject.toml b/pyproject.toml index 0576bac779c..405da83b2b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,6 +126,13 @@ google = [ "google-auth-oauthlib>=1.0,<2", "google-auth-httplib2>=0.2,<1", ] +youtube = [ + # Required by skills/media/youtube-content and + # optional-skills/productivity/memento-flashcards (youtube_quiz.py). + # Without this declaration uv sync omits the package and both skills fail + # at first invocation with ModuleNotFoundError (issue #22243). + "youtube-transcript-api>=1.2.0", +] # `hermes dashboard` (localhost SPA + API). Not in core to keep the default install lean. web = ["fastapi>=0.104.0,<1", "uvicorn[standard]>=0.24.0,<1"] rl = [ @@ -163,6 +170,7 @@ all = [ "hermes-agent[mistral]", "hermes-agent[bedrock]", "hermes-agent[web]", + "hermes-agent[youtube]", ] [project.scripts] diff --git a/tests/skills/test_fetch_transcript.py b/tests/skills/test_fetch_transcript.py new file mode 100644 index 00000000000..4196eab9cce --- /dev/null +++ b/tests/skills/test_fetch_transcript.py @@ -0,0 +1,87 @@ +"""Tests for skills/media/youtube-content/scripts/fetch_transcript.py (issue #22243).""" + +import sys +from pathlib import Path +from unittest import mock + +import pytest + +SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "skills" / "media" / "youtube-content" / "scripts" +sys.path.insert(0, str(SCRIPTS_DIR)) + +import fetch_transcript + + +class TestExtractVideoId: + def test_standard_watch_url(self): + assert fetch_transcript.extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ") == "dQw4w9WgXcQ" + + def test_short_url(self): + assert fetch_transcript.extract_video_id("https://youtu.be/dQw4w9WgXcQ") == "dQw4w9WgXcQ" + + def test_bare_video_id(self): + assert fetch_transcript.extract_video_id("dQw4w9WgXcQ") == "dQw4w9WgXcQ" + + def test_shorts_url(self): + assert fetch_transcript.extract_video_id("https://www.youtube.com/shorts/dQw4w9WgXcQ") == "dQw4w9WgXcQ" + + def test_embed_url(self): + assert fetch_transcript.extract_video_id("https://www.youtube.com/embed/dQw4w9WgXcQ") == "dQw4w9WgXcQ" + + def test_with_extra_params(self): + assert fetch_transcript.extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42") == "dQw4w9WgXcQ" + + +class TestFormatTimestamp: + def test_seconds_only(self): + assert fetch_transcript.format_timestamp(90) == "1:30" + + def test_with_hours(self): + assert fetch_transcript.format_timestamp(3661) == "1:01:01" + + def test_zero(self): + assert fetch_transcript.format_timestamp(0) == "0:00" + + def test_minutes_only(self): + assert fetch_transcript.format_timestamp(600) == "10:00" + + +class TestFetchTranscriptImportError: + def test_missing_dep_exits_with_message(self, capsys): + """fetch_transcript exits with code 1 and prints install hint when package missing (issue #22243).""" + import builtins + real_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == "youtube_transcript_api": + raise ImportError("No module named 'youtube_transcript_api'") + return real_import(name, *args, **kwargs) + + with mock.patch("builtins.__import__", side_effect=mock_import): + with pytest.raises(SystemExit) as exc_info: + fetch_transcript.fetch_transcript("dQw4w9WgXcQ") + assert exc_info.value.code == 1 + captured = capsys.readouterr() + assert "youtube-transcript-api" in captured.err + + +class TestPyprojectDeclaresYoutubeExtra: + def test_youtube_extra_declared_in_pyproject(self): + """youtube-transcript-api must be listed in pyproject.toml [youtube] extra (issue #22243).""" + import tomllib + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + with pyproject_path.open("rb") as f: + data = tomllib.load(f) + extras = data.get("project", {}).get("optional-dependencies", {}) + assert "youtube" in extras, "Missing [youtube] extra in pyproject.toml" + youtube_deps = " ".join(extras["youtube"]) + assert "youtube-transcript-api" in youtube_deps + + def test_youtube_extra_included_in_all(self): + """[all] extra must include hermes-agent[youtube] (issue #22243).""" + import tomllib + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + with pyproject_path.open("rb") as f: + data = tomllib.load(f) + all_deps = " ".join(data["project"]["optional-dependencies"].get("all", [])) + assert "youtube" in all_deps, "[all] extra does not include hermes-agent[youtube]"