crewai-transcriptapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TranscriptAPI (ZeroPointRepo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: crewai-transcriptapi
3
+ Version: 0.1.0
4
+ Summary: CrewAI tools for TranscriptAPI: fetch YouTube transcripts and search YouTube videos and channels from CrewAI agents.
5
+ Project-URL: Homepage, https://transcriptapi.com
6
+ Project-URL: Documentation, https://transcriptapi.com/docs
7
+ Project-URL: Repository, https://github.com/ZeroPointRepo/crewai-transcriptapi
8
+ Author-email: Zero Point Studio <hello@transcriptapi.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 TranscriptAPI (ZeroPointRepo)
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: ai-agent,crewai,crewai-tools,llm,transcript,transcriptapi,youtube,youtube-api,youtube-transcript
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Requires-Python: >=3.10
41
+ Requires-Dist: crewai>=0.80.0
42
+ Requires-Dist: pydantic>=2.0.0
43
+ Requires-Dist: requests>=2.31.0
44
+ Description-Content-Type: text/markdown
45
+
46
+ # crewai-transcriptapi
47
+
48
+ **TranscriptAPI: hosted YouTube transcript + video-discovery API for AI agents.** CrewAI tools edition. Also available as an [n8n community node](https://github.com/ZeroPointRepo/n8n-nodes-transcriptapi), an [MCP server](https://github.com/ZeroPointRepo/youtube-mcp) and [agent skills](https://github.com/ZeroPointRepo/youtube-skills).
49
+
50
+ This package gives [CrewAI](https://www.crewai.com/) agents two tools over [TranscriptAPI](https://transcriptapi.com):
51
+
52
+ - **TranscriptAPITool**: fetch the transcript of any YouTube video (full URL, youtu.be, Shorts, or bare ID) as plain text or timestamped JSON, with metadata. The hero tool: 1 credit per call.
53
+ - **TranscriptAPISearchTool**: search YouTube for videos or channels to discover content before fetching transcripts. 1 credit per page.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install crewai-transcriptapi
59
+ ```
60
+
61
+ ## Credentials
62
+
63
+ You need a TranscriptAPI key (starts with `sk_`):
64
+
65
+ 1. Create an account at [transcriptapi.com](https://transcriptapi.com): **100 free credits, no card (one-time)**; paid plans from **$5/mo (1,000 credits)**.
66
+ 2. Create an API key on the dashboard.
67
+ 3. Export it:
68
+
69
+ ```bash
70
+ export TRANSCRIPTAPI_API_KEY="sk_..."
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ```python
76
+ from crewai import Agent
77
+ from crewai_transcriptapi import TranscriptAPITool, TranscriptAPISearchTool
78
+
79
+ researcher = Agent(
80
+ role="Video researcher",
81
+ goal="Find and summarize YouTube content on a topic",
82
+ backstory="Researches spoken video content via transcripts.",
83
+ tools=[TranscriptAPISearchTool(), TranscriptAPITool()],
84
+ )
85
+ ```
86
+
87
+ The agent can then search for videos on a topic and fetch each transcript for summarizing, quoting, translating or analysis.
88
+
89
+ ## Tool reference
90
+
91
+ ### TranscriptAPITool
92
+
93
+ | Argument | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `video_url` | str | required | Full YouTube URL (watch, youtu.be, embed, Shorts) or bare 11-character video ID |
96
+ | `output_format` | str | `"text"` | `"text"` for plain transcript, `"json"` for timestamped segments |
97
+ | `language` | str | none | Optional preferred transcript language code, for example `"es"` |
98
+
99
+ ### TranscriptAPISearchTool
100
+
101
+ | Argument | Type | Default | Description |
102
+ |---|---|---|---|
103
+ | `query` | str | required | Search query, 1 to 200 characters |
104
+ | `search_type` | str | `"video"` | `"video"` or `"channel"` |
105
+
106
+ ## Response envelope
107
+
108
+ Both tools return a stable JSON string and never raise:
109
+
110
+ ```json
111
+ {"success": true, "data": {"transcript": "...", "metadata": {"title": "..."}}}
112
+ ```
113
+
114
+ ```json
115
+ {"success": false, "error": {"code": "out_of_credits", "message": "The account is out of credits. See https://transcriptapi.com/billing."}}
116
+ ```
117
+
118
+ Error codes: `missing_api_key`, `invalid_api_key`, `out_of_credits`, `not_found`, `rate_limited`, `network`, `bad_response`, `http_<status>`. Failed calls are never charged.
119
+
120
+ ## Resources
121
+
122
+ - [TranscriptAPI documentation](https://transcriptapi.com/docs)
123
+ - [Pricing](https://transcriptapi.com)
124
+ - Family: [n8n node](https://github.com/ZeroPointRepo/n8n-nodes-transcriptapi) · [MCP server](https://github.com/ZeroPointRepo/youtube-mcp) · [agent skills](https://github.com/ZeroPointRepo/youtube-skills)
125
+
126
+ ## Disclosure
127
+
128
+ TranscriptAPI is an independent product and is not affiliated with or endorsed by YouTube or Google. Use of these tools is subject to the [TranscriptAPI terms](https://transcriptapi.com/terms).
129
+
130
+ ## License
131
+
132
+ [MIT](LICENSE)
@@ -0,0 +1,87 @@
1
+ # crewai-transcriptapi
2
+
3
+ **TranscriptAPI: hosted YouTube transcript + video-discovery API for AI agents.** CrewAI tools edition. Also available as an [n8n community node](https://github.com/ZeroPointRepo/n8n-nodes-transcriptapi), an [MCP server](https://github.com/ZeroPointRepo/youtube-mcp) and [agent skills](https://github.com/ZeroPointRepo/youtube-skills).
4
+
5
+ This package gives [CrewAI](https://www.crewai.com/) agents two tools over [TranscriptAPI](https://transcriptapi.com):
6
+
7
+ - **TranscriptAPITool**: fetch the transcript of any YouTube video (full URL, youtu.be, Shorts, or bare ID) as plain text or timestamped JSON, with metadata. The hero tool: 1 credit per call.
8
+ - **TranscriptAPISearchTool**: search YouTube for videos or channels to discover content before fetching transcripts. 1 credit per page.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install crewai-transcriptapi
14
+ ```
15
+
16
+ ## Credentials
17
+
18
+ You need a TranscriptAPI key (starts with `sk_`):
19
+
20
+ 1. Create an account at [transcriptapi.com](https://transcriptapi.com): **100 free credits, no card (one-time)**; paid plans from **$5/mo (1,000 credits)**.
21
+ 2. Create an API key on the dashboard.
22
+ 3. Export it:
23
+
24
+ ```bash
25
+ export TRANSCRIPTAPI_API_KEY="sk_..."
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from crewai import Agent
32
+ from crewai_transcriptapi import TranscriptAPITool, TranscriptAPISearchTool
33
+
34
+ researcher = Agent(
35
+ role="Video researcher",
36
+ goal="Find and summarize YouTube content on a topic",
37
+ backstory="Researches spoken video content via transcripts.",
38
+ tools=[TranscriptAPISearchTool(), TranscriptAPITool()],
39
+ )
40
+ ```
41
+
42
+ The agent can then search for videos on a topic and fetch each transcript for summarizing, quoting, translating or analysis.
43
+
44
+ ## Tool reference
45
+
46
+ ### TranscriptAPITool
47
+
48
+ | Argument | Type | Default | Description |
49
+ |---|---|---|---|
50
+ | `video_url` | str | required | Full YouTube URL (watch, youtu.be, embed, Shorts) or bare 11-character video ID |
51
+ | `output_format` | str | `"text"` | `"text"` for plain transcript, `"json"` for timestamped segments |
52
+ | `language` | str | none | Optional preferred transcript language code, for example `"es"` |
53
+
54
+ ### TranscriptAPISearchTool
55
+
56
+ | Argument | Type | Default | Description |
57
+ |---|---|---|---|
58
+ | `query` | str | required | Search query, 1 to 200 characters |
59
+ | `search_type` | str | `"video"` | `"video"` or `"channel"` |
60
+
61
+ ## Response envelope
62
+
63
+ Both tools return a stable JSON string and never raise:
64
+
65
+ ```json
66
+ {"success": true, "data": {"transcript": "...", "metadata": {"title": "..."}}}
67
+ ```
68
+
69
+ ```json
70
+ {"success": false, "error": {"code": "out_of_credits", "message": "The account is out of credits. See https://transcriptapi.com/billing."}}
71
+ ```
72
+
73
+ Error codes: `missing_api_key`, `invalid_api_key`, `out_of_credits`, `not_found`, `rate_limited`, `network`, `bad_response`, `http_<status>`. Failed calls are never charged.
74
+
75
+ ## Resources
76
+
77
+ - [TranscriptAPI documentation](https://transcriptapi.com/docs)
78
+ - [Pricing](https://transcriptapi.com)
79
+ - Family: [n8n node](https://github.com/ZeroPointRepo/n8n-nodes-transcriptapi) · [MCP server](https://github.com/ZeroPointRepo/youtube-mcp) · [agent skills](https://github.com/ZeroPointRepo/youtube-skills)
80
+
81
+ ## Disclosure
82
+
83
+ TranscriptAPI is an independent product and is not affiliated with or endorsed by YouTube or Google. Use of these tools is subject to the [TranscriptAPI terms](https://transcriptapi.com/terms).
84
+
85
+ ## License
86
+
87
+ [MIT](LICENSE)
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "crewai-transcriptapi"
7
+ version = "0.1.0"
8
+ description = "CrewAI tools for TranscriptAPI: fetch YouTube transcripts and search YouTube videos and channels from CrewAI agents."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Zero Point Studio", email = "hello@transcriptapi.com" }]
13
+ keywords = [
14
+ "crewai",
15
+ "crewai-tools",
16
+ "transcriptapi",
17
+ "youtube",
18
+ "youtube-transcript",
19
+ "transcript",
20
+ "youtube-api",
21
+ "ai-agent",
22
+ "llm",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ ]
34
+ dependencies = ["crewai>=0.80.0", "requests>=2.31.0", "pydantic>=2.0.0"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://transcriptapi.com"
38
+ Documentation = "https://transcriptapi.com/docs"
39
+ Repository = "https://github.com/ZeroPointRepo/crewai-transcriptapi"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/crewai_transcriptapi"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,21 @@
1
+ """crewai-transcriptapi: CrewAI tools for TranscriptAPI (transcriptapi.com)."""
2
+
3
+ from .transcriptapi_tool import (
4
+ TranscriptAPISearchToolSchema,
5
+ TranscriptAPIToolSchema,
6
+ )
7
+
8
+ try:
9
+ from .transcriptapi_tool import TranscriptAPISearchTool, TranscriptAPITool
10
+ except ImportError: # pragma: no cover - crewai not installed
11
+ pass
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "TranscriptAPITool",
17
+ "TranscriptAPISearchTool",
18
+ "TranscriptAPIToolSchema",
19
+ "TranscriptAPISearchToolSchema",
20
+ "__version__",
21
+ ]
@@ -0,0 +1,182 @@
1
+ """CrewAI tools for TranscriptAPI (transcriptapi.com).
2
+
3
+ Two tools following the crewai-tools BaseTool conventions:
4
+
5
+ - TranscriptAPITool: fetch the transcript of a YouTube video (the hero use case)
6
+ - TranscriptAPISearchTool: search YouTube for videos or channels
7
+
8
+ Both return a stable JSON string envelope and never leak exceptions:
9
+ {"success": true, "data": {...}}
10
+ {"success": false, "error": {"code": "...", "message": "..."}}
11
+
12
+ Auth: TRANSCRIPTAPI_API_KEY environment variable (Bearer key, "sk_..." format).
13
+ """
14
+
15
+ import json
16
+ import os
17
+ from typing import Any, Optional, Type
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+ try: # crewai is the runtime host; keep the import lazy-friendly for tests
22
+ from crewai.tools import BaseTool, EnvVar
23
+ except ImportError: # pragma: no cover - exercised only without crewai installed
24
+ BaseTool = None # type: ignore[assignment,misc]
25
+ EnvVar = None # type: ignore[assignment,misc]
26
+
27
+ BASE_URL = "https://transcriptapi.com/api/v2"
28
+ USER_AGENT = "crewai-transcriptapi/0.1.0 (+https://github.com/ZeroPointRepo/crewai-transcriptapi)"
29
+ TIMEOUT_SECONDS = 60
30
+
31
+ _ENV_KEY = "TRANSCRIPTAPI_API_KEY"
32
+
33
+ _ENV_VARS = (
34
+ [
35
+ EnvVar(
36
+ name=_ENV_KEY,
37
+ description=(
38
+ "TranscriptAPI key (starts with sk_). Create one at "
39
+ "https://transcriptapi.com: 100 free credits, no card (one-time)."
40
+ ),
41
+ required=True,
42
+ )
43
+ ]
44
+ if EnvVar is not None
45
+ else []
46
+ )
47
+
48
+
49
+ def _envelope_error(code: str, message: str) -> str:
50
+ return json.dumps({"success": False, "error": {"code": code, "message": message}})
51
+
52
+
53
+ def _request(path: str, params: dict) -> str:
54
+ """GET a TranscriptAPI endpoint and return the JSON string envelope."""
55
+ key = os.environ.get(_ENV_KEY, "").strip()
56
+ if not key:
57
+ return _envelope_error(
58
+ "missing_api_key",
59
+ f"{_ENV_KEY} is not set. Get a free key at https://transcriptapi.com "
60
+ "(100 free credits, no card, one-time).",
61
+ )
62
+ import requests # lazy import so tests can stub it
63
+
64
+ try:
65
+ response = requests.get(
66
+ BASE_URL + path,
67
+ params={k: v for k, v in params.items() if v is not None},
68
+ headers={
69
+ "Authorization": f"Bearer {key}",
70
+ "User-Agent": USER_AGENT,
71
+ "Accept": "application/json",
72
+ },
73
+ timeout=TIMEOUT_SECONDS,
74
+ )
75
+ except requests.exceptions.RequestException as exc:
76
+ return _envelope_error("network", str(exc))
77
+ if response.status_code != 200:
78
+ code = {
79
+ 401: "invalid_api_key",
80
+ 402: "out_of_credits",
81
+ 404: "not_found",
82
+ 429: "rate_limited",
83
+ }.get(response.status_code, f"http_{response.status_code}")
84
+ message = {
85
+ 401: "The API key was rejected. Check TRANSCRIPTAPI_API_KEY.",
86
+ 402: "The account is out of credits. See https://transcriptapi.com/billing.",
87
+ 404: "Not found: the video may not exist or has no captions.",
88
+ 429: "Rate limited. Wait and retry, respecting the Retry-After header.",
89
+ }.get(response.status_code, response.text[:300])
90
+ return _envelope_error(code, message)
91
+ try:
92
+ data = response.json()
93
+ except ValueError:
94
+ return _envelope_error("bad_response", "The API returned a non-JSON response.")
95
+ return json.dumps({"success": True, "data": data})
96
+
97
+
98
+ class TranscriptAPIToolSchema(BaseModel):
99
+ """Input for TranscriptAPITool."""
100
+
101
+ video_url: str = Field(
102
+ ...,
103
+ min_length=6,
104
+ description=(
105
+ "Full YouTube URL (watch, youtu.be, embed, or Shorts) or the bare "
106
+ "11-character video ID"
107
+ ),
108
+ )
109
+ output_format: str = Field(
110
+ default="text",
111
+ pattern="^(text|json)$",
112
+ description='Transcript format: "text" (plain transcript) or "json" (timestamped segments)',
113
+ )
114
+ language: Optional[str] = Field(
115
+ default=None,
116
+ description="Optional preferred transcript language code, for example 'es'",
117
+ )
118
+
119
+
120
+ class TranscriptAPISearchToolSchema(BaseModel):
121
+ """Input for TranscriptAPISearchTool."""
122
+
123
+ query: str = Field(..., min_length=1, max_length=200, description="Search query")
124
+ search_type: str = Field(
125
+ default="video",
126
+ pattern="^(video|channel)$",
127
+ description='What to search for: "video" or "channel"',
128
+ )
129
+
130
+
131
+ if BaseTool is not None:
132
+
133
+ class TranscriptAPITool(BaseTool):
134
+ """Fetch the transcript of a YouTube video via TranscriptAPI."""
135
+
136
+ model_config = ConfigDict(arbitrary_types_allowed=True)
137
+ name: str = "TranscriptAPI YouTube Transcript"
138
+ description: str = (
139
+ "Fetches the transcript of a YouTube video, with metadata, via the "
140
+ "TranscriptAPI service. Accepts a full YouTube URL (watch, youtu.be, "
141
+ "embed, Shorts) or a bare 11-character video ID. Use when the task "
142
+ "needs the spoken content of a specific video: summarizing, quoting, "
143
+ "translating or analyzing it. Costs 1 credit per successful call."
144
+ )
145
+ args_schema: Type[BaseModel] = TranscriptAPIToolSchema
146
+ package_dependencies: list = Field(default_factory=lambda: ["requests"])
147
+ env_vars: list = Field(default_factory=lambda: list(_ENV_VARS))
148
+
149
+ def _run(self, **kwargs: Any) -> str:
150
+ args = TranscriptAPIToolSchema(**kwargs)
151
+ return _request(
152
+ "/youtube/transcript",
153
+ {
154
+ "video_url": args.video_url,
155
+ "format": args.output_format,
156
+ "send_metadata": "true",
157
+ "language": args.language,
158
+ },
159
+ )
160
+
161
+ class TranscriptAPISearchTool(BaseTool):
162
+ """Search YouTube for videos or channels via TranscriptAPI."""
163
+
164
+ model_config = ConfigDict(arbitrary_types_allowed=True)
165
+ name: str = "TranscriptAPI YouTube Search"
166
+ description: str = (
167
+ "Searches YouTube for videos or channels via the TranscriptAPI "
168
+ "service and returns titles, IDs, thumbnails, view counts and "
169
+ "publish dates. Use when the task needs to discover videos or "
170
+ "channels about a topic before fetching transcripts. Costs 1 credit "
171
+ "per page of results."
172
+ )
173
+ args_schema: Type[BaseModel] = TranscriptAPISearchToolSchema
174
+ package_dependencies: list = Field(default_factory=lambda: ["requests"])
175
+ env_vars: list = Field(default_factory=lambda: list(_ENV_VARS))
176
+
177
+ def _run(self, **kwargs: Any) -> str:
178
+ args = TranscriptAPISearchToolSchema(**kwargs)
179
+ return _request(
180
+ "/youtube/search",
181
+ {"q": args.query, "type": args.search_type},
182
+ )
@@ -0,0 +1,175 @@
1
+ """Tests for crewai-transcriptapi. No network, no crewai install required.
2
+
3
+ crewai.tools is stubbed with faithful pydantic-based stand-ins BEFORE the tool
4
+ module imports (the DB2 test pattern: stub optional externals so import-time
5
+ validation still runs through real pydantic).
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ import types
12
+ import unittest
13
+ from unittest import mock
14
+
15
+ from pydantic import BaseModel
16
+
17
+ # ── stub crewai.tools before importing the module under test ─────────────────
18
+ crewai_mod = types.ModuleType("crewai")
19
+ tools_mod = types.ModuleType("crewai.tools")
20
+
21
+
22
+ class BaseTool(BaseModel):
23
+ """Faithful minimal stand-in: pydantic model with a run() entrypoint."""
24
+
25
+ name: str = ""
26
+ description: str = ""
27
+
28
+ def run(self, **kwargs):
29
+ return self._run(**kwargs)
30
+
31
+
32
+ class EnvVar(BaseModel):
33
+ name: str
34
+ description: str = ""
35
+ required: bool = False
36
+
37
+
38
+ tools_mod.BaseTool = BaseTool
39
+ tools_mod.EnvVar = EnvVar
40
+ crewai_mod.tools = tools_mod
41
+ sys.modules.setdefault("crewai", crewai_mod)
42
+ sys.modules["crewai.tools"] = tools_mod
43
+
44
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
45
+
46
+ from crewai_transcriptapi.transcriptapi_tool import ( # noqa: E402
47
+ TranscriptAPISearchTool,
48
+ TranscriptAPISearchToolSchema,
49
+ TranscriptAPITool,
50
+ TranscriptAPIToolSchema,
51
+ )
52
+
53
+
54
+ def _fake_response(status=200, payload=None, text=""):
55
+ resp = mock.Mock()
56
+ resp.status_code = status
57
+ resp.text = text
58
+ if payload is None:
59
+ resp.json.side_effect = ValueError("no json")
60
+ else:
61
+ resp.json.return_value = payload
62
+ return resp
63
+
64
+
65
+ class SchemaTests(unittest.TestCase):
66
+ def test_transcript_schema_requires_video_url(self):
67
+ with self.assertRaises(Exception):
68
+ TranscriptAPIToolSchema()
69
+
70
+ def test_transcript_schema_rejects_bad_format(self):
71
+ with self.assertRaises(Exception):
72
+ TranscriptAPIToolSchema(video_url="dQw4w9WgXcQ", output_format="xml")
73
+
74
+ def test_transcript_schema_defaults(self):
75
+ s = TranscriptAPIToolSchema(video_url="dQw4w9WgXcQ")
76
+ self.assertEqual(s.output_format, "text")
77
+ self.assertIsNone(s.language)
78
+
79
+ def test_search_schema_bounds(self):
80
+ with self.assertRaises(Exception):
81
+ TranscriptAPISearchToolSchema(query="")
82
+ with self.assertRaises(Exception):
83
+ TranscriptAPISearchToolSchema(query="x", search_type="playlist")
84
+
85
+
86
+ class EnvTests(unittest.TestCase):
87
+ def test_missing_key_returns_envelope_not_exception(self):
88
+ with mock.patch.dict(os.environ, {}, clear=True):
89
+ out = json.loads(TranscriptAPITool().run(video_url="dQw4w9WgXcQ"))
90
+ self.assertFalse(out["success"])
91
+ self.assertEqual(out["error"]["code"], "missing_api_key")
92
+
93
+ def test_env_vars_declared(self):
94
+ tool = TranscriptAPITool()
95
+ self.assertEqual(tool.env_vars[0].name, "TRANSCRIPTAPI_API_KEY")
96
+ self.assertTrue(tool.env_vars[0].required)
97
+
98
+
99
+ class RequestTests(unittest.TestCase):
100
+ def _run_with(self, resp, **kwargs):
101
+ with mock.patch.dict(os.environ, {"TRANSCRIPTAPI_API_KEY": "sk_test"}, clear=True):
102
+ with mock.patch("requests.get", return_value=resp) as getter:
103
+ out = json.loads(TranscriptAPITool().run(**kwargs))
104
+ return out, getter
105
+
106
+ def test_request_construction(self):
107
+ resp = _fake_response(payload={"transcript": "hello", "video_id": "dQw4w9WgXcQ"})
108
+ out, getter = self._run_with(
109
+ resp, video_url="dQw4w9WgXcQ", output_format="json", language="es"
110
+ )
111
+ self.assertTrue(out["success"])
112
+ url = getter.call_args.args[0]
113
+ kwargs = getter.call_args.kwargs
114
+ self.assertEqual(url, "https://transcriptapi.com/api/v2/youtube/transcript")
115
+ self.assertEqual(kwargs["params"]["video_url"], "dQw4w9WgXcQ")
116
+ self.assertEqual(kwargs["params"]["format"], "json")
117
+ self.assertEqual(kwargs["params"]["language"], "es")
118
+ self.assertEqual(kwargs["headers"]["Authorization"], "Bearer sk_test")
119
+ self.assertIn("crewai-transcriptapi", kwargs["headers"]["User-Agent"])
120
+
121
+ def test_language_omitted_when_none(self):
122
+ resp = _fake_response(payload={"transcript": "hi"})
123
+ _, getter = self._run_with(resp, video_url="dQw4w9WgXcQ")
124
+ self.assertNotIn("language", getter.call_args.kwargs["params"])
125
+
126
+ def test_success_envelope(self):
127
+ resp = _fake_response(payload={"transcript": "hello world"})
128
+ out, _ = self._run_with(resp, video_url="dQw4w9WgXcQ")
129
+ self.assertTrue(out["success"])
130
+ self.assertEqual(out["data"]["transcript"], "hello world")
131
+
132
+ def test_402_maps_to_out_of_credits(self):
133
+ out, _ = self._run_with(_fake_response(status=402), video_url="dQw4w9WgXcQ")
134
+ self.assertFalse(out["success"])
135
+ self.assertEqual(out["error"]["code"], "out_of_credits")
136
+
137
+ def test_404_and_429_and_unknown(self):
138
+ for status, code in ((404, "not_found"), (429, "rate_limited"), (500, "http_500")):
139
+ out, _ = self._run_with(_fake_response(status=status), video_url="dQw4w9WgXcQ")
140
+ self.assertEqual(out["error"]["code"], code)
141
+
142
+ def test_network_error_envelope(self):
143
+ import requests
144
+
145
+ with mock.patch.dict(os.environ, {"TRANSCRIPTAPI_API_KEY": "sk_test"}, clear=True):
146
+ with mock.patch(
147
+ "requests.get",
148
+ side_effect=requests.exceptions.ConnectionError("boom"),
149
+ ):
150
+ out = json.loads(TranscriptAPITool().run(video_url="dQw4w9WgXcQ"))
151
+ self.assertEqual(out["error"]["code"], "network")
152
+
153
+ def test_non_json_body_envelope(self):
154
+ out, _ = self._run_with(_fake_response(payload=None), video_url="dQw4w9WgXcQ")
155
+ self.assertEqual(out["error"]["code"], "bad_response")
156
+
157
+
158
+ class SearchToolTests(unittest.TestCase):
159
+ def test_search_request_and_envelope(self):
160
+ resp = _fake_response(payload={"results": [{"title": "a video"}]})
161
+ with mock.patch.dict(os.environ, {"TRANSCRIPTAPI_API_KEY": "sk_test"}, clear=True):
162
+ with mock.patch("requests.get", return_value=resp) as getter:
163
+ out = json.loads(
164
+ TranscriptAPISearchTool().run(query="machine learning", search_type="channel")
165
+ )
166
+ self.assertTrue(out["success"])
167
+ self.assertEqual(
168
+ getter.call_args.args[0], "https://transcriptapi.com/api/v2/youtube/search"
169
+ )
170
+ self.assertEqual(getter.call_args.kwargs["params"]["q"], "machine learning")
171
+ self.assertEqual(getter.call_args.kwargs["params"]["type"], "channel")
172
+
173
+
174
+ if __name__ == "__main__":
175
+ unittest.main()