Skip to content

Commit 71e8cd8

Browse files
author
Chandler Casey
committed
2.4.0: add sync and async monitor lifecycle and event pagination
1 parent 498965f commit 71e8cd8

13 files changed

Lines changed: 1154 additions & 8 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ jobs:
1919
- uses: actions/setup-python@v5
2020
with:
2121
python-version: "3.12"
22+
- name: Test
23+
run: |
24+
python -m pip install -e ".[dev]"
25+
ruff check .
26+
mypy src
27+
pytest -q
2228
- name: Build
2329
run: |
2430
python -m pip install --upgrade pip build

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
All notable changes to this project are documented here. This project adheres to
44
[Semantic Versioning](https://semver.org).
55

6+
## [2.4.0] - 2026-09-27
7+
8+
### Added
9+
10+
- Monitor create, list, get, update/pause/resume, delete, check-now, events and
11+
automatic event pagination. Includes typed settings, news cursors, delivery
12+
state, inline/follow-up transcript results and check errors. Creation exposes
13+
the one-time webhook secret. Create/check share the existing idempotent retry
14+
transport. Python includes sync and async methods.
15+
- Single-video options for mode, timestamps and callback URL, including
16+
captions-only and plain-text requests generated by the Playground.
17+
- HTTP contract tests for monitor requests/responses, partial updates, pagination,
18+
errors and retries; release workflows run the tests before publishing.
19+
620
## [2.3.0] - 2026-09-15
721

822
### Added

‎README.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,52 @@ pending = [r.job_id for r in res.results if r.outcome == "processing"]
7777
res = tf.transcripts.batch(ids, mode="captions") # captions only, no audio fallback
7878
```
7979

80+
## Monitors (2.4.0+)
81+
82+
Watch a YouTube channel or a TikTok/Instagram profile for new videos. Creation
83+
records a free baseline and starts scheduled checks; existing videos are skipped.
84+
Checks with no new videos are free. A check finding videos costs 1 credit, plus
85+
transcript charges when enabled. See [monitor docs](https://transcriptfetch.com/docs/monitors)
86+
for plan limits, audio billing, retention and webhook verification.
87+
88+
```python
89+
with TranscriptFetch(timeout=120) as tf:
90+
monitor = tf.monitors.create("@lexfridman", interval_minutes=60, transcripts=True)
91+
# Save monitor.webhook_secret securely: it is only returned on creation.
92+
# Optional webhook_url="https://your-app.com/hooks/transcriptfetch".
93+
# YouTube only: tab="shorts" (or "videos" / "live").
94+
all_monitors = tf.monitors.list()
95+
news = tf.monitors.list(since=all_monitors.last_event_id)
96+
current = tf.monitors.get(monitor.id)
97+
page = tf.monitors.events(monitor.id, limit=10)
98+
for event in tf.monitors.iter_events(monitor.id, since=current.last_event_id):
99+
print(event.type, event.data)
100+
tf.monitors.update(monitor.id, status="paused")
101+
tf.monitors.update(monitor.id, webhook_url=None, name=None, transcripts=False)
102+
# Omitted settings stay unchanged. None clears webhook_url/name.
103+
tf.monitors.update(monitor.id, status="active")
104+
checked = tf.monitors.check(monitor.id) # may spend credits
105+
if checked.error: # listing failure, even with HTTP 200
106+
print(checked.error)
107+
tf.monitors.delete(monitor.id) # removes events too
108+
```
109+
110+
`AsyncTranscriptFetch` exposes the same methods with `await`; use `async for`
111+
with `tf.monitors.iter_events(...)`. Models use snake_case. `monitor.videos`
112+
events carry `data.videos` and optional `data.transcripts` with `ok`, `processing`
113+
or `error` outcomes. `monitor.transcript` events carry the follow-up result
114+
in `data`, linked to the parent via `videos_event_id`. `delivery` describes
115+
webhook attempts. Reading never marks events read: persist the latest event id
116+
and pass it as `since`. Iteration preserves `since` across cursor pages.
117+
Create/check accept `idempotency_key`; the generated key stays stable on retries.
118+
Use a longer client timeout for creation/manual checks, which read the platform.
119+
120+
Single-video options are also supported:
121+
122+
```python
123+
transcript = tf.transcripts.video("dQw4w9WgXcQ", mode="captions", timestamps=False)
124+
```
125+
80126
## Pagination
81127

82128
List endpoints are cursor-paginated. Iterate every result without managing cursors:

‎src/transcriptfetch/__init__.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@
3333
ApiErrorBlock,
3434
BatchResponse,
3535
BatchResult,
36+
Monitor,
37+
MonitorCheck,
38+
MonitorDeleted,
39+
MonitorDelivery,
40+
MonitorEvent,
41+
MonitorEventList,
42+
MonitorInterval,
43+
MonitorLastError,
44+
MonitorLimits,
45+
MonitorList,
46+
MonitorSnapshot,
47+
MonitorTranscriptData,
48+
MonitorTranscriptEvent,
49+
MonitorTranscriptResult,
50+
MonitorVideosData,
51+
MonitorVideosEvent,
3652
Segment,
3753
Transcript,
3854
Usage,
@@ -45,6 +61,22 @@
4561
"__version__",
4662
"TranscriptFetch",
4763
"AsyncTranscriptFetch",
64+
"Monitor",
65+
"MonitorSnapshot",
66+
"MonitorLastError",
67+
"MonitorLimits",
68+
"MonitorList",
69+
"MonitorDeleted",
70+
"MonitorDelivery",
71+
"MonitorTranscriptResult",
72+
"MonitorVideosData",
73+
"MonitorTranscriptData",
74+
"MonitorVideosEvent",
75+
"MonitorTranscriptEvent",
76+
"MonitorEvent",
77+
"MonitorEventList",
78+
"MonitorCheck",
79+
"MonitorInterval",
4880
# models
4981
"Usage",
5082
"Account",

‎src/transcriptfetch/_version.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.3.0"
1+
__version__ = "2.4.0"

‎src/transcriptfetch/async_client.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from .errors import APIConnectionError, APITimeoutError
2121
from .models import Account
22+
from .resources.monitors import AsyncMonitors
2223
from .resources.transcripts import AsyncTranscripts
2324

2425

@@ -48,6 +49,7 @@ def __init__(
4849
self._http = http_client or httpx.AsyncClient(
4950
base_url=self._config.base_url, timeout=self._config.timeout
5051
)
52+
self.monitors = AsyncMonitors(self)
5153
self.transcripts = AsyncTranscripts(self)
5254

5355
async def _request(

‎src/transcriptfetch/client.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from .errors import APIConnectionError, APITimeoutError
2121
from .models import Account
22+
from .resources.monitors import Monitors
2223
from .resources.transcripts import Transcripts
2324

2425

@@ -45,6 +46,7 @@ def __init__(
4546
self._http = http_client or httpx.Client(
4647
base_url=self._config.base_url, timeout=self._config.timeout
4748
)
49+
self.monitors = Monitors(self)
4850
self.transcripts = Transcripts(self)
4951

5052
def _request(

‎src/transcriptfetch/models.py‎

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from __future__ import annotations
99

10-
from typing import Any, Dict, List, Literal, Optional
10+
from typing import Any, Dict, List, Literal, Optional, Union
1111

1212
from pydantic import BaseModel, ConfigDict, Field, field_validator
1313

@@ -201,3 +201,126 @@ class BatchResponse(_Model):
201201
kind: Literal["transcript_batch"] = "transcript_batch"
202202
results: List[BatchResult] = Field(default_factory=list)
203203
usage: Optional[Usage] = None
204+
205+
206+
# Monitor response models -----------------------------------------------------
207+
MonitorInterval = Literal[15, 60, 360, 1440]
208+
209+
210+
class MonitorSnapshot(_Model):
211+
id: str
212+
name: Optional[str] = None
213+
type: str = "channel"
214+
platform: str = "youtube"
215+
target: str = ""
216+
217+
218+
class MonitorLastError(ApiErrorBlock):
219+
at: Optional[str] = None
220+
221+
222+
class Monitor(MonitorSnapshot):
223+
"""A scheduled monitor. webhook_secret and baseline_count appear only on creation."""
224+
225+
kind: str = "monitor"
226+
options: Dict[str, Any] = Field(default_factory=dict)
227+
status: str = "active"
228+
has_new: bool = False
229+
last_event_id: Optional[str] = None
230+
interval_minutes: int = 60
231+
transcripts: bool = False
232+
webhook_url: Optional[str] = None
233+
next_check_at: Optional[str] = None
234+
last_checked_at: Optional[str] = None
235+
last_error: Optional[MonitorLastError] = None
236+
created_at: Optional[str] = None
237+
updated_at: Optional[str] = None
238+
webhook_secret: Optional[str] = Field(default=None, repr=False)
239+
baseline_count: Optional[int] = None
240+
usage: Optional[Usage] = None
241+
242+
243+
class MonitorLimits(_Model):
244+
max_monitors: Optional[int] = None
245+
min_interval_minutes: int = 60
246+
247+
248+
class MonitorList(_Model):
249+
kind: str = "monitor_list"
250+
monitors: List[Monitor] = Field(default_factory=list)
251+
has_new: bool = False
252+
last_event_id: Optional[str] = None
253+
limits: MonitorLimits = Field(default_factory=MonitorLimits)
254+
255+
256+
class MonitorDeleted(_Model):
257+
kind: str = "monitor_deleted"
258+
id: str
259+
260+
261+
class MonitorDelivery(_Model):
262+
status: str = "none"
263+
attempts: int = 0
264+
last_attempt_at: Optional[str] = None
265+
next_attempt_at: Optional[str] = None
266+
last_error: Optional[str] = None
267+
delivered_at: Optional[str] = None
268+
269+
270+
class MonitorTranscriptResult(_Model):
271+
job_id: Optional[str] = None
272+
video_id: str
273+
url: str = ""
274+
outcome: str
275+
transcript: Optional[Transcript] = None
276+
error: Optional[ApiErrorBlock] = None
277+
278+
279+
class MonitorVideosData(_Model):
280+
videos: List[Video] = Field(default_factory=list)
281+
transcripts: Optional[List[MonitorTranscriptResult]] = None
282+
283+
284+
class MonitorTranscriptData(MonitorTranscriptResult):
285+
videos_event_id: str
286+
287+
288+
class MonitorVideosEvent(_Model):
289+
id: str
290+
type: Literal["monitor.videos"]
291+
created_at: str
292+
monitor: MonitorSnapshot
293+
credits_spent: int = 0
294+
data: MonitorVideosData
295+
delivery: Optional[MonitorDelivery] = None
296+
297+
298+
class MonitorTranscriptEvent(_Model):
299+
id: str
300+
type: Literal["monitor.transcript"]
301+
created_at: str
302+
monitor: MonitorSnapshot
303+
credits_spent: int = 0
304+
data: MonitorTranscriptData
305+
delivery: Optional[MonitorDelivery] = None
306+
307+
308+
MonitorEvent = Union[MonitorVideosEvent, MonitorTranscriptEvent]
309+
310+
311+
class MonitorEventList(_Model):
312+
kind: str = "monitor_event_list"
313+
monitor_id: str
314+
events: List[MonitorEvent] = Field(default_factory=list)
315+
next_cursor: Optional[str] = None
316+
317+
318+
class MonitorCheck(_Model):
319+
"""A check may return HTTP 200 with error set when the listing failed."""
320+
321+
kind: str = "monitor_check"
322+
monitor: Monitor
323+
new_videos: int = 0
324+
event: Optional[MonitorEvent] = None
325+
error: Optional[ApiErrorBlock] = None
326+
usage: Optional[Usage] = None
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .monitors import AsyncMonitors, Monitors
12
from .transcripts import AsyncTranscripts, Transcripts
23

3-
__all__ = ["Transcripts", "AsyncTranscripts"]
4+
__all__ = ["Transcripts", "AsyncTranscripts", "Monitors", "AsyncMonitors"]

0 commit comments

Comments
 (0)