Skip to content

Commit 81ef0f3

Browse files
author
Chandler Casey
committed
Initial commit: TranscriptFetch Python SDK v0.1.0
Sync + async httpx clients, Pydantic v2 models, typed error hierarchy, auto-pagination, retries with auto Idempotency-Key. Covers video/channel/ playlist/search/batch + health. Fully tested (respx), ruff + mypy clean.
0 parents  commit 81ef0f3

27 files changed

Lines changed: 1478 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
- name: Install
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install -e ".[dev]"
24+
- name: Lint (ruff)
25+
run: ruff check .
26+
- name: Type check (mypy)
27+
run: mypy src
28+
- name: Test (pytest)
29+
run: pytest -q

‎.github/workflows/release.yml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Release
2+
3+
# Publishes to PyPI on a version tag (e.g. v0.1.0) using PyPI Trusted Publishing
4+
# (OIDC): no API tokens stored. Configure the publisher on PyPI first:
5+
# https://docs.pypi.org/trusted-publishers/
6+
7+
on:
8+
push:
9+
tags: ["v*"]
10+
11+
jobs:
12+
build-and-publish:
13+
runs-on: ubuntu-latest
14+
environment: pypi
15+
permissions:
16+
id-token: write # required for trusted publishing
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
- name: Build
23+
run: |
24+
python -m pip install --upgrade pip build
25+
python -m build
26+
- name: Publish to PyPI
27+
uses: pypa/gh-action-pypi-publish@release/v1

‎.gitignore‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.eggs/
6+
build/
7+
dist/
8+
*.egg
9+
10+
# Envs
11+
.venv/
12+
venv/
13+
env/
14+
15+
# Tooling caches
16+
.pytest_cache/
17+
.mypy_cache/
18+
.ruff_cache/
19+
.coverage
20+
htmlcov/
21+
22+
# Editors / OS
23+
.idea/
24+
.vscode/
25+
.DS_Store

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented here. This project adheres to
4+
[Semantic Versioning](https://semver.org).
5+
6+
## [0.1.0] - Unreleased
7+
8+
Initial release.
9+
10+
- Sync `TranscriptFetch` and async `AsyncTranscriptFetch` clients (httpx).
11+
- `transcripts.video / channel / playlist / search / batch` + `health()`.
12+
- Auto-paginating `iter_channel / iter_playlist / iter_search`.
13+
- Typed Pydantic v2 models (`Transcript`, `VideoList`, `Video`, `Segment`, `BatchResponse`, `Usage`).
14+
- Typed exception hierarchy mapped from the API's canonical error codes.
15+
- Automatic retries with backoff (429 + 5xx) and auto Idempotency-Key.

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 TranscriptFetch
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.

‎README.md‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# TranscriptFetch Python SDK
2+
3+
Official, typed Python client for the [TranscriptFetch](https://transcriptfetch.com) API: fetch YouTube transcripts, channels, playlists, and search results as clean, structured data. Sync + async, fully type-hinted.
4+
5+
```bash
6+
pip install transcriptfetch-sdk
7+
```
8+
9+
## Quickstart
10+
11+
```python
12+
from transcriptfetch import TranscriptFetch
13+
14+
# api_key falls back to the TRANSCRIPTFETCH_API_KEY env var
15+
tf = TranscriptFetch(api_key="tf_live_...")
16+
17+
t = tf.transcripts.video("https://youtu.be/aircAruvnKk")
18+
print(t.title)
19+
print(t.text)
20+
for seg in t.segments:
21+
print(f"[{seg.start:.1f}] {seg.text}")
22+
23+
print("credits left:", t.usage.balance)
24+
```
25+
26+
Get an API key (100 free credits) at <https://transcriptfetch.com/app>. One credit per successful fetch; failed/blocked/no-transcript requests are free.
27+
28+
## Endpoints
29+
30+
```python
31+
tf.transcripts.video(video) # single transcript (text + segments)
32+
tf.transcripts.channel(channel, limit=, cursor=) # a channel's videos (metadata)
33+
tf.transcripts.playlist(playlist, limit=, cursor=) # a playlist's videos
34+
tf.transcripts.search(query, limit=, cursor=) # search YouTube
35+
tf.transcripts.batch(video_ids) # up to 50 transcripts in one call
36+
tf.health() # unauthenticated liveness probe
37+
```
38+
39+
`video`/`channel`/`playlist` accept URLs or raw IDs (normalized automatically).
40+
41+
## Pagination
42+
43+
List endpoints are cursor-paginated. Iterate every result without managing cursors:
44+
45+
```python
46+
for video in tf.transcripts.iter_channel("@lexfridman", limit=10):
47+
print(video.video_id, video.title)
48+
```
49+
50+
Or page manually via `page.next_cursor` and the `cursor=` argument.
51+
52+
## Async
53+
54+
```python
55+
import asyncio
56+
from transcriptfetch import AsyncTranscriptFetch
57+
58+
async def main():
59+
async with AsyncTranscriptFetch() as tf:
60+
t = await tf.transcripts.video("aircAruvnKk")
61+
print(t.text)
62+
async for v in tf.transcripts.iter_search("how transformers work", limit=10):
63+
print(v.title)
64+
65+
asyncio.run(main())
66+
```
67+
68+
## Errors
69+
70+
All errors subclass `TranscriptFetchError`. API errors carry `.status`, `.code`, `.message`, and `.request_id`:
71+
72+
```python
73+
from transcriptfetch import (
74+
AuthenticationError, InsufficientCreditsError, InvalidRequestError,
75+
RateLimitError, IdempotencyConflictError, UpstreamUnavailableError,
76+
InternalServerError, APIError, APIConnectionError, APITimeoutError,
77+
)
78+
79+
try:
80+
tf.transcripts.video("bad")
81+
except InsufficientCreditsError:
82+
... # 402: top up at /pricing
83+
except RateLimitError as e:
84+
print(e.retry_after) # 429
85+
except APIError as e:
86+
print(e.status, e.code, e.request_id)
87+
```
88+
89+
## Reliability
90+
91+
- **Automatic retries** on `429` (honoring `Retry-After`) and `5xx`, with exponential backoff + jitter (`max_retries=2` by default).
92+
- **Idempotency**: every write auto-sends an `Idempotency-Key` so a retried request is never double-charged. Override per call with `idempotency_key=...`.
93+
- **Configurable:** `TranscriptFetch(api_key=..., base_url=..., timeout=30, max_retries=2)`. Both clients are context managers and accept a custom `http_client=` (httpx).
94+
95+
## Development
96+
97+
```bash
98+
pip install -e ".[dev]"
99+
ruff check . && mypy src && pytest
100+
```
101+
102+
Tests are fully mocked (no network). MIT licensed.

‎examples/async_example.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Async usage. TRANSCRIPTFETCH_API_KEY=tf_live_... python examples/async_example.py"""
2+
3+
import asyncio
4+
5+
from transcriptfetch import AsyncTranscriptFetch
6+
7+
8+
async def main() -> None:
9+
async with AsyncTranscriptFetch() as tf:
10+
t = await tf.transcripts.video("aircAruvnKk")
11+
print((t.text or "(no transcript)")[:200])
12+
13+
14+
asyncio.run(main())

‎examples/batch.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Fetch up to 50 transcripts in one call."""
2+
3+
from transcriptfetch import TranscriptFetch
4+
5+
with TranscriptFetch() as tf:
6+
res = tf.transcripts.batch(["aircAruvnKk", "Tn6-PIqc4UM"])
7+
for r in res.results:
8+
print(r.video_id, r.outcome, "-", (r.text or "")[:60])
9+
if res.usage:
10+
print("credits spent:", res.usage.credits_spent)

‎examples/paginate.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Auto-paginate through a channel's videos (follows next_cursor for you)."""
2+
3+
from transcriptfetch import TranscriptFetch
4+
5+
with TranscriptFetch() as tf:
6+
for i, video in enumerate(tf.transcripts.iter_channel("@lexfridman", limit=10)):
7+
print(video.video_id, "-", video.title)
8+
if i >= 24: # stop after 25
9+
break

‎examples/quickstart.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Fetch a single transcript.
2+
3+
Run with your key set: TRANSCRIPTFETCH_API_KEY=tf_live_... python examples/quickstart.py
4+
"""
5+
6+
from transcriptfetch import TranscriptFetch
7+
8+
with TranscriptFetch() as tf: # reads TRANSCRIPTFETCH_API_KEY
9+
t = tf.transcripts.video("https://youtu.be/aircAruvnKk")
10+
print("title:", t.title)
11+
print("text:", (t.text or "(no transcript)")[:500])
12+
print("segments:", len(t.segments))
13+
if t.usage:
14+
print(f"credits spent: {t.usage.credits_spent} · balance: {t.usage.balance}")

0 commit comments

Comments
 (0)