transcodely 0.3.1__tar.gz → 0.3.2__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.
- {transcodely-0.3.1 → transcodely-0.3.2}/PKG-INFO +87 -4
- {transcodely-0.3.1 → transcodely-0.3.2}/README.md +85 -2
- {transcodely-0.3.1 → transcodely-0.3.2}/pyproject.toml +7 -2
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/__init__.py +20 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/transport.py +4 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/client.py +16 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/apps.py +47 -0
- transcodely-0.3.2/src/transcodely/resources/billing.py +92 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/jobs.py +33 -6
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/memberships.py +3 -2
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/users.py +3 -2
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/videos.py +39 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/types.py +86 -0
- transcodely-0.3.2/src/transcodely/v1/app_pb2.py +111 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/app_pb2.pyi +48 -4
- transcodely-0.3.2/src/transcodely/v1/billing_pb2.py +65 -0
- transcodely-0.3.2/src/transcodely/v1/billing_pb2.pyi +131 -0
- transcodely-0.3.2/src/transcodely/v1/job_pb2.py +226 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/job_pb2.pyi +70 -8
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.py +13 -7
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.pyi +56 -3
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/thumbnails_pb2.py +14 -8
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/thumbnails_pb2.pyi +10 -2
- transcodely-0.3.2/src/transcodely/v1/video_pb2.py +210 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/video_pb2.pyi +168 -10
- transcodely-0.3.2/src/transcodely/v1/watermark_pb2.py +57 -0
- transcodely-0.3.2/src/transcodely/v1/watermark_pb2.pyi +56 -0
- transcodely-0.3.2/src/transcodely/v1/webhook_pb2.py +155 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/webhook_pb2.pyi +22 -6
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/version.py +1 -1
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/types.py +28 -4
- transcodely-0.3.1/src/transcodely/v1/app_pb2.py +0 -97
- transcodely-0.3.1/src/transcodely/v1/job_pb2.py +0 -205
- transcodely-0.3.1/src/transcodely/v1/video_pb2.py +0 -168
- transcodely-0.3.1/src/transcodely/v1/webhook_pb2.py +0 -151
- {transcodely-0.3.1 → transcodely-0.3.2}/.gitignore +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/LICENSE +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/validate_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/validate_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_codec/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_codec/json_codec.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/error_mapping.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/headers.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/errors.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/pagination.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/py.typed +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/_helpers.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/api_keys.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/events.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/health.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/organizations.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/origins.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/presets.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/webhook_endpoints.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/streaming.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.pyi +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/__init__.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/construct_event.py +0 -0
- {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/signature.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transcodely
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Official Python SDK for the Transcodely video transcoding API
|
|
5
5
|
Project-URL: Homepage, https://transcodely.com
|
|
6
6
|
Project-URL: Documentation, https://transcodely.com/docs
|
|
@@ -30,7 +30,7 @@ Provides-Extra: dev
|
|
|
30
30
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
31
31
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
32
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
-
Requires-Dist: ruff
|
|
33
|
+
Requires-Dist: ruff<0.16,>=0.5; extra == 'dev'
|
|
34
34
|
Provides-Extra: fast
|
|
35
35
|
Requires-Dist: orjson>=3.10; extra == 'fast'
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
@@ -52,6 +52,7 @@ from transcodely import Transcodely
|
|
|
52
52
|
with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
53
53
|
job = client.jobs.create(
|
|
54
54
|
input_url="https://example.com/source.mp4",
|
|
55
|
+
managed=True, # write outputs to Transcodely-managed storage
|
|
55
56
|
outputs=[{
|
|
56
57
|
"type": "hls",
|
|
57
58
|
"video": [
|
|
@@ -59,6 +60,10 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
59
60
|
{"codec": "h264", "resolution": "720p"},
|
|
60
61
|
],
|
|
61
62
|
}],
|
|
63
|
+
# Optional: encode only a sub-range of the input. Applies job-wide and
|
|
64
|
+
# reduces cost (billing keys off the produced output duration). Omit
|
|
65
|
+
# "end_seconds" (or leave 0) to encode through to the end of the input.
|
|
66
|
+
clip={"start_seconds": 2, "end_seconds": 7},
|
|
62
67
|
)
|
|
63
68
|
print(job.id) # "job_a1b2c3d4e5f6"
|
|
64
69
|
|
|
@@ -70,6 +75,35 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
70
75
|
|
|
71
76
|
The simplified-string form (`"hls"`, `"h264"`, `"1080p"`) is what the API actually emits over the wire — the SDK round-trips it transparently to and from the proto enum integers.
|
|
72
77
|
|
|
78
|
+
## AI captions
|
|
79
|
+
|
|
80
|
+
Add auto-generated captions to any output with a `generate` subtitle track. Leave `language` empty (or set `"auto"`) to auto-detect the spoken language, or pass an ISO 639-2 code to force one. A per-job fee is metered by source minute and surfaced on `job.fees`; produced captions appear on `job.subtitle_results` with `auto_generated=True`.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Generate captions while transcoding a new source.
|
|
84
|
+
client.jobs.create(
|
|
85
|
+
input_url="https://example.com/source.mp4",
|
|
86
|
+
managed=True,
|
|
87
|
+
outputs=[{
|
|
88
|
+
"type": "hls",
|
|
89
|
+
"video": [{"codec": "h264", "resolution": "1080p"}],
|
|
90
|
+
"subtitle_tracks": [{"operation": "generate", "language": "auto"}],
|
|
91
|
+
}],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Retro-caption a video you've already hosted: reference it by input_video_id and
|
|
95
|
+
# request a single captions-only output (no video encode).
|
|
96
|
+
job = client.jobs.create(
|
|
97
|
+
input_video_id="vid_a1b2c3d4e5f6g7",
|
|
98
|
+
outputs=[{"subtitle_tracks": [{"operation": "generate"}]}],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for result in job.subtitle_results:
|
|
102
|
+
print(result.language, result.auto_generated, result.url)
|
|
103
|
+
for fee in job.fees:
|
|
104
|
+
print(fee.fee_type, fee.amount, fee.currency) # "captions" 0.51 "eur"
|
|
105
|
+
```
|
|
106
|
+
|
|
73
107
|
## Authentication
|
|
74
108
|
|
|
75
109
|
```python
|
|
@@ -82,7 +116,7 @@ API keys are opaque `ak_`-prefixed secrets — pass the full value shown once at
|
|
|
82
116
|
|
|
83
117
|
```python
|
|
84
118
|
client.jobs # create / get / list / cancel / confirm / watch
|
|
85
|
-
client.videos # upload helpers, multipart, get / list / update / delete / watch
|
|
119
|
+
client.videos # upload helpers, multipart, create_from_url, get / list / update / delete / watch, get_stats / list_top_videos
|
|
86
120
|
client.presets # create / get / get_by_slug / list / update / duplicate / archive
|
|
87
121
|
client.origins # create / get / list / update / validate / archive
|
|
88
122
|
client.apps # create / get / list / update / archive / enable_hosting
|
|
@@ -96,6 +130,55 @@ client.events # retrieve / list / resend
|
|
|
96
130
|
client.webhooks # construct_event / verify_signature (signature-verification helpers)
|
|
97
131
|
```
|
|
98
132
|
|
|
133
|
+
## Videos
|
|
134
|
+
|
|
135
|
+
### One-call ingest from a URL
|
|
136
|
+
|
|
137
|
+
`create_from_url` skips the upload dance entirely: give it a publicly-reachable
|
|
138
|
+
`http(s)` URL and the worker fetches it at transcode time. Requires an app with
|
|
139
|
+
managed hosting enabled (`client.apps.enable_hosting(...)`).
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
video = client.videos.create_from_url(
|
|
143
|
+
app_id="app_default000",
|
|
144
|
+
url="https://example.com/source.mp4",
|
|
145
|
+
title="Product demo",
|
|
146
|
+
)
|
|
147
|
+
print(video.id, video.status) # "processing" — no upload step
|
|
148
|
+
|
|
149
|
+
for event in client.videos.watch(video.id):
|
|
150
|
+
if event.video.status == "ready":
|
|
151
|
+
print(event.video.playback_url, event.video.embed_url)
|
|
152
|
+
break
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
No `video.uploaded` event fires for URL ingest (no bytes are uploaded to the
|
|
156
|
+
API) — subscribe to `video.ready` / `video.failed` instead to know when it's
|
|
157
|
+
playable.
|
|
158
|
+
|
|
159
|
+
### Playback analytics
|
|
160
|
+
|
|
161
|
+
`get_stats` returns a single video's playback stats — plays, watch time, and
|
|
162
|
+
unique viewers — aggregated per UTC day, plus range totals. `list_top_videos`
|
|
163
|
+
ranks an app's videos by plays over a date range. Both accept optional
|
|
164
|
+
`start_date` / `end_date` (`YYYY-MM-DD`, inclusive); stats are best-effort and
|
|
165
|
+
roll up hourly, so recent activity can lag by up to an hour.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
stats = client.videos.get_stats(
|
|
169
|
+
video_id="vid_abc123def456",
|
|
170
|
+
start_date="2026-07-01",
|
|
171
|
+
end_date="2026-07-16",
|
|
172
|
+
)
|
|
173
|
+
for day in stats.daily:
|
|
174
|
+
print(day.date, day.plays, day.watch_seconds, day.unique_viewers)
|
|
175
|
+
print("range totals:", stats.totals.plays, stats.totals.watch_seconds)
|
|
176
|
+
|
|
177
|
+
top = client.videos.list_top_videos(app_id="app_default000", limit=10)
|
|
178
|
+
for video in top.items:
|
|
179
|
+
print(video.video_id, video.title, video.plays, video.watch_seconds)
|
|
180
|
+
```
|
|
181
|
+
|
|
99
182
|
## Origins
|
|
100
183
|
|
|
101
184
|
An origin tells Transcodely where to read source media from and where to write outputs. Every origin belongs to a single provider; pass exactly one provider-config field (`s3`, `gcs`, `http`, or `r2`) on create.
|
|
@@ -314,7 +397,7 @@ for event in client.events.list(app_id="app_123").auto_paging_iter():
|
|
|
314
397
|
client.events.resend("evt_123") # re-queue delivery to all subscribed endpoints
|
|
315
398
|
```
|
|
316
399
|
|
|
317
|
-
The
|
|
400
|
+
The 17 event types are `job.created`, `job.succeeded`, `job.failed`, `job.canceled`, `job.progress`, `output.created`, `output.ready`, `output.failed`, `output.progress`, `video.uploaded`, `video.ready`, `video.failed`, `video.deleted`, `app.created`, `app.updated`, `app.spend_limit_warning`, and `app.spend_limit_exceeded`. Subscribe to `"*"` to receive all of them (including ones added later). An unrecognized future type still verifies; its `event.data` is left as a plain `dict`. The two `app.spend_limit_*` events also carry a plain `dict` (`app_id`, `period_start`, `period_end`, `limit_eur`, `spent_eur`, `threshold_pct`, `currency`), not a resource snapshot.
|
|
318
401
|
|
|
319
402
|
## Configuration
|
|
320
403
|
|
|
@@ -15,6 +15,7 @@ from transcodely import Transcodely
|
|
|
15
15
|
with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
16
16
|
job = client.jobs.create(
|
|
17
17
|
input_url="https://example.com/source.mp4",
|
|
18
|
+
managed=True, # write outputs to Transcodely-managed storage
|
|
18
19
|
outputs=[{
|
|
19
20
|
"type": "hls",
|
|
20
21
|
"video": [
|
|
@@ -22,6 +23,10 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
22
23
|
{"codec": "h264", "resolution": "720p"},
|
|
23
24
|
],
|
|
24
25
|
}],
|
|
26
|
+
# Optional: encode only a sub-range of the input. Applies job-wide and
|
|
27
|
+
# reduces cost (billing keys off the produced output duration). Omit
|
|
28
|
+
# "end_seconds" (or leave 0) to encode through to the end of the input.
|
|
29
|
+
clip={"start_seconds": 2, "end_seconds": 7},
|
|
25
30
|
)
|
|
26
31
|
print(job.id) # "job_a1b2c3d4e5f6"
|
|
27
32
|
|
|
@@ -33,6 +38,35 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
33
38
|
|
|
34
39
|
The simplified-string form (`"hls"`, `"h264"`, `"1080p"`) is what the API actually emits over the wire — the SDK round-trips it transparently to and from the proto enum integers.
|
|
35
40
|
|
|
41
|
+
## AI captions
|
|
42
|
+
|
|
43
|
+
Add auto-generated captions to any output with a `generate` subtitle track. Leave `language` empty (or set `"auto"`) to auto-detect the spoken language, or pass an ISO 639-2 code to force one. A per-job fee is metered by source minute and surfaced on `job.fees`; produced captions appear on `job.subtitle_results` with `auto_generated=True`.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Generate captions while transcoding a new source.
|
|
47
|
+
client.jobs.create(
|
|
48
|
+
input_url="https://example.com/source.mp4",
|
|
49
|
+
managed=True,
|
|
50
|
+
outputs=[{
|
|
51
|
+
"type": "hls",
|
|
52
|
+
"video": [{"codec": "h264", "resolution": "1080p"}],
|
|
53
|
+
"subtitle_tracks": [{"operation": "generate", "language": "auto"}],
|
|
54
|
+
}],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Retro-caption a video you've already hosted: reference it by input_video_id and
|
|
58
|
+
# request a single captions-only output (no video encode).
|
|
59
|
+
job = client.jobs.create(
|
|
60
|
+
input_video_id="vid_a1b2c3d4e5f6g7",
|
|
61
|
+
outputs=[{"subtitle_tracks": [{"operation": "generate"}]}],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
for result in job.subtitle_results:
|
|
65
|
+
print(result.language, result.auto_generated, result.url)
|
|
66
|
+
for fee in job.fees:
|
|
67
|
+
print(fee.fee_type, fee.amount, fee.currency) # "captions" 0.51 "eur"
|
|
68
|
+
```
|
|
69
|
+
|
|
36
70
|
## Authentication
|
|
37
71
|
|
|
38
72
|
```python
|
|
@@ -45,7 +79,7 @@ API keys are opaque `ak_`-prefixed secrets — pass the full value shown once at
|
|
|
45
79
|
|
|
46
80
|
```python
|
|
47
81
|
client.jobs # create / get / list / cancel / confirm / watch
|
|
48
|
-
client.videos # upload helpers, multipart, get / list / update / delete / watch
|
|
82
|
+
client.videos # upload helpers, multipart, create_from_url, get / list / update / delete / watch, get_stats / list_top_videos
|
|
49
83
|
client.presets # create / get / get_by_slug / list / update / duplicate / archive
|
|
50
84
|
client.origins # create / get / list / update / validate / archive
|
|
51
85
|
client.apps # create / get / list / update / archive / enable_hosting
|
|
@@ -59,6 +93,55 @@ client.events # retrieve / list / resend
|
|
|
59
93
|
client.webhooks # construct_event / verify_signature (signature-verification helpers)
|
|
60
94
|
```
|
|
61
95
|
|
|
96
|
+
## Videos
|
|
97
|
+
|
|
98
|
+
### One-call ingest from a URL
|
|
99
|
+
|
|
100
|
+
`create_from_url` skips the upload dance entirely: give it a publicly-reachable
|
|
101
|
+
`http(s)` URL and the worker fetches it at transcode time. Requires an app with
|
|
102
|
+
managed hosting enabled (`client.apps.enable_hosting(...)`).
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
video = client.videos.create_from_url(
|
|
106
|
+
app_id="app_default000",
|
|
107
|
+
url="https://example.com/source.mp4",
|
|
108
|
+
title="Product demo",
|
|
109
|
+
)
|
|
110
|
+
print(video.id, video.status) # "processing" — no upload step
|
|
111
|
+
|
|
112
|
+
for event in client.videos.watch(video.id):
|
|
113
|
+
if event.video.status == "ready":
|
|
114
|
+
print(event.video.playback_url, event.video.embed_url)
|
|
115
|
+
break
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
No `video.uploaded` event fires for URL ingest (no bytes are uploaded to the
|
|
119
|
+
API) — subscribe to `video.ready` / `video.failed` instead to know when it's
|
|
120
|
+
playable.
|
|
121
|
+
|
|
122
|
+
### Playback analytics
|
|
123
|
+
|
|
124
|
+
`get_stats` returns a single video's playback stats — plays, watch time, and
|
|
125
|
+
unique viewers — aggregated per UTC day, plus range totals. `list_top_videos`
|
|
126
|
+
ranks an app's videos by plays over a date range. Both accept optional
|
|
127
|
+
`start_date` / `end_date` (`YYYY-MM-DD`, inclusive); stats are best-effort and
|
|
128
|
+
roll up hourly, so recent activity can lag by up to an hour.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
stats = client.videos.get_stats(
|
|
132
|
+
video_id="vid_abc123def456",
|
|
133
|
+
start_date="2026-07-01",
|
|
134
|
+
end_date="2026-07-16",
|
|
135
|
+
)
|
|
136
|
+
for day in stats.daily:
|
|
137
|
+
print(day.date, day.plays, day.watch_seconds, day.unique_viewers)
|
|
138
|
+
print("range totals:", stats.totals.plays, stats.totals.watch_seconds)
|
|
139
|
+
|
|
140
|
+
top = client.videos.list_top_videos(app_id="app_default000", limit=10)
|
|
141
|
+
for video in top.items:
|
|
142
|
+
print(video.video_id, video.title, video.plays, video.watch_seconds)
|
|
143
|
+
```
|
|
144
|
+
|
|
62
145
|
## Origins
|
|
63
146
|
|
|
64
147
|
An origin tells Transcodely where to read source media from and where to write outputs. Every origin belongs to a single provider; pass exactly one provider-config field (`s3`, `gcs`, `http`, or `r2`) on create.
|
|
@@ -277,7 +360,7 @@ for event in client.events.list(app_id="app_123").auto_paging_iter():
|
|
|
277
360
|
client.events.resend("evt_123") # re-queue delivery to all subscribed endpoints
|
|
278
361
|
```
|
|
279
362
|
|
|
280
|
-
The
|
|
363
|
+
The 17 event types are `job.created`, `job.succeeded`, `job.failed`, `job.canceled`, `job.progress`, `output.created`, `output.ready`, `output.failed`, `output.progress`, `video.uploaded`, `video.ready`, `video.failed`, `video.deleted`, `app.created`, `app.updated`, `app.spend_limit_warning`, and `app.spend_limit_exceeded`. Subscribe to `"*"` to receive all of them (including ones added later). An unrecognized future type still verifies; its `event.data` is left as a plain `dict`. The two `app.spend_limit_*` events also carry a plain `dict` (`app_id`, `period_start`, `period_end`, `limit_eur`, `spent_eur`, `threshold_pct`, `currency`), not a resource snapshot.
|
|
281
364
|
|
|
282
365
|
## Configuration
|
|
283
366
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "transcodely"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Official Python SDK for the Transcodely video transcoding API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -37,7 +37,12 @@ fast = ["orjson>=3.10"]
|
|
|
37
37
|
dev = [
|
|
38
38
|
"pytest>=8.0",
|
|
39
39
|
"pytest-asyncio>=0.23",
|
|
40
|
-
|
|
40
|
+
# Upper bound is deliberate: ruff 0.16 promoted UP007/UP045 (and others) into
|
|
41
|
+
# its default rule set, which flags the Optional[...]/Union[...] style used
|
|
42
|
+
# throughout this repo — 257 findings on an untouched master. Adopting the
|
|
43
|
+
# new rules is a mechanical `ruff check --fix` sweep that belongs in its own
|
|
44
|
+
# PR, not in whatever change happens to run CI first after a ruff release.
|
|
45
|
+
"ruff>=0.5,<0.16",
|
|
41
46
|
"mypy>=1.10",
|
|
42
47
|
]
|
|
43
48
|
|
|
@@ -68,11 +68,18 @@ from .types import (
|
|
|
68
68
|
App,
|
|
69
69
|
AudioTrackConfig,
|
|
70
70
|
AutoABRConfig,
|
|
71
|
+
ChapterPoint,
|
|
72
|
+
ChapterResult,
|
|
73
|
+
ClipConfig,
|
|
71
74
|
ContentAwareConfig,
|
|
72
75
|
DASHConfig,
|
|
73
76
|
DRMConfig,
|
|
74
77
|
HDRConfig,
|
|
75
78
|
HLSConfig,
|
|
79
|
+
Invoice,
|
|
80
|
+
InvoiceLineItem,
|
|
81
|
+
InvoiceLineType,
|
|
82
|
+
InvoiceStatus,
|
|
76
83
|
Job,
|
|
77
84
|
JobOutput,
|
|
78
85
|
JobPriority,
|
|
@@ -87,6 +94,7 @@ from .types import (
|
|
|
87
94
|
PresetVariant,
|
|
88
95
|
Resolution,
|
|
89
96
|
StreamingConfig,
|
|
97
|
+
SubtitleResult,
|
|
90
98
|
SubtitleTrack,
|
|
91
99
|
ThumbnailResult,
|
|
92
100
|
ThumbnailSpec,
|
|
@@ -95,10 +103,12 @@ from .types import (
|
|
|
95
103
|
Video,
|
|
96
104
|
VideoCodec,
|
|
97
105
|
VideoRendition,
|
|
106
|
+
VideoTextTrack,
|
|
98
107
|
VideoVariant,
|
|
99
108
|
WatchEventType,
|
|
100
109
|
WatchJobResponse,
|
|
101
110
|
WatchVideoResponse,
|
|
111
|
+
WatermarkConfig,
|
|
102
112
|
WebhookDelivery,
|
|
103
113
|
WebhookEndpoint,
|
|
104
114
|
)
|
|
@@ -112,6 +122,9 @@ __all__ = [
|
|
|
112
122
|
"AudioTrackConfig",
|
|
113
123
|
"AuthenticationError",
|
|
114
124
|
"AutoABRConfig",
|
|
125
|
+
"ChapterPoint",
|
|
126
|
+
"ChapterResult",
|
|
127
|
+
"ClipConfig",
|
|
115
128
|
"ConflictError",
|
|
116
129
|
"construct_event",
|
|
117
130
|
"ContentAwareConfig",
|
|
@@ -127,6 +140,10 @@ __all__ = [
|
|
|
127
140
|
"HDRConfig",
|
|
128
141
|
"HLSConfig",
|
|
129
142
|
"InvalidRequestError",
|
|
143
|
+
"Invoice",
|
|
144
|
+
"InvoiceLineItem",
|
|
145
|
+
"InvoiceLineType",
|
|
146
|
+
"InvoiceStatus",
|
|
130
147
|
"Job",
|
|
131
148
|
"JobOutput",
|
|
132
149
|
"JobPriority",
|
|
@@ -149,6 +166,7 @@ __all__ = [
|
|
|
149
166
|
"SDK_VERSION",
|
|
150
167
|
"SIGNATURE_HEADER",
|
|
151
168
|
"StreamingConfig",
|
|
169
|
+
"SubtitleResult",
|
|
152
170
|
"SubtitleTrack",
|
|
153
171
|
"ThumbnailResult",
|
|
154
172
|
"ThumbnailSpec",
|
|
@@ -160,10 +178,12 @@ __all__ = [
|
|
|
160
178
|
"Video",
|
|
161
179
|
"VideoCodec",
|
|
162
180
|
"VideoRendition",
|
|
181
|
+
"VideoTextTrack",
|
|
163
182
|
"VideoVariant",
|
|
164
183
|
"WatchEventType",
|
|
165
184
|
"WatchJobResponse",
|
|
166
185
|
"WatchVideoResponse",
|
|
186
|
+
"WatermarkConfig",
|
|
167
187
|
"WebhookDelivery",
|
|
168
188
|
"WebhookEndpoint",
|
|
169
189
|
"WebhookError",
|
|
@@ -71,6 +71,7 @@ class Transport:
|
|
|
71
71
|
self,
|
|
72
72
|
api_key: str,
|
|
73
73
|
*,
|
|
74
|
+
organization_id: Optional[str] = None,
|
|
74
75
|
base_url: Optional[str] = None,
|
|
75
76
|
timeout: float = 30.0,
|
|
76
77
|
max_retries: int = 3,
|
|
@@ -82,6 +83,7 @@ class Transport:
|
|
|
82
83
|
if not api_key:
|
|
83
84
|
raise ValueError("Transcodely: api_key is required")
|
|
84
85
|
self.api_key = api_key
|
|
86
|
+
self.organization_id = organization_id
|
|
85
87
|
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
86
88
|
self.timeout = timeout
|
|
87
89
|
self.max_retries = max_retries
|
|
@@ -218,6 +220,8 @@ class Transport:
|
|
|
218
220
|
"transcodely-version": opts.api_version or self.api_version,
|
|
219
221
|
"accept": content_type,
|
|
220
222
|
}
|
|
223
|
+
if self.organization_id:
|
|
224
|
+
headers["x-organization-id"] = self.organization_id
|
|
221
225
|
if idempotency_key:
|
|
222
226
|
headers["idempotency-key"] = idempotency_key
|
|
223
227
|
headers.update(self.default_headers)
|
|
@@ -9,6 +9,7 @@ import httpx
|
|
|
9
9
|
from ._transport.transport import LogEvent, Transport
|
|
10
10
|
from .resources.api_keys import ApiKeys
|
|
11
11
|
from .resources.apps import Apps
|
|
12
|
+
from .resources.billing import Billing
|
|
12
13
|
from .resources.events import Events
|
|
13
14
|
from .resources.health import Health
|
|
14
15
|
from .resources.jobs import Jobs
|
|
@@ -37,6 +38,7 @@ class Transcodely:
|
|
|
37
38
|
self,
|
|
38
39
|
api_key: str,
|
|
39
40
|
*,
|
|
41
|
+
organization_id: Optional[str] = None,
|
|
40
42
|
base_url: Optional[str] = None,
|
|
41
43
|
timeout: float = 30.0,
|
|
42
44
|
max_retries: int = 3,
|
|
@@ -47,6 +49,7 @@ class Transcodely:
|
|
|
47
49
|
) -> None:
|
|
48
50
|
self._transport = Transport(
|
|
49
51
|
api_key,
|
|
52
|
+
organization_id=organization_id,
|
|
50
53
|
base_url=base_url,
|
|
51
54
|
timeout=timeout,
|
|
52
55
|
max_retries=max_retries,
|
|
@@ -67,6 +70,7 @@ class Transcodely:
|
|
|
67
70
|
self._health: Optional[Health] = None
|
|
68
71
|
self._webhook_endpoints: Optional[WebhookEndpoints] = None
|
|
69
72
|
self._events: Optional[Events] = None
|
|
73
|
+
self._billing: Optional[Billing] = None
|
|
70
74
|
|
|
71
75
|
def close(self) -> None:
|
|
72
76
|
self._transport.close()
|
|
@@ -154,6 +158,18 @@ class Transcodely:
|
|
|
154
158
|
self._events = Events(self._transport)
|
|
155
159
|
return self._events
|
|
156
160
|
|
|
161
|
+
@property
|
|
162
|
+
def billing(self) -> Billing:
|
|
163
|
+
"""The organization's invoices.
|
|
164
|
+
|
|
165
|
+
Needs a dashboard session token for an organization owner plus
|
|
166
|
+
``organization_id`` on the client; API keys are rejected. See
|
|
167
|
+
:class:`~transcodely.resources.billing.Billing`.
|
|
168
|
+
"""
|
|
169
|
+
if self._billing is None:
|
|
170
|
+
self._billing = Billing(self._transport)
|
|
171
|
+
return self._billing
|
|
172
|
+
|
|
157
173
|
@property
|
|
158
174
|
def webhooks(self) -> type[Webhooks]:
|
|
159
175
|
"""Stripe-style facade: ``client.webhooks.construct_event(body, sig, secret)``."""
|
|
@@ -69,3 +69,50 @@ class Apps:
|
|
|
69
69
|
return self._t.unary(
|
|
70
70
|
_SERVICE, "UpdateHostingConfig", req, app_pb2.UpdateHostingConfigResponse()
|
|
71
71
|
)
|
|
72
|
+
|
|
73
|
+
def update_spend_limit(
|
|
74
|
+
self,
|
|
75
|
+
app_id: str,
|
|
76
|
+
monthly_spend_limit_eur: Optional[float] = None,
|
|
77
|
+
opts: Optional[CallOptions] = None,
|
|
78
|
+
) -> app_pb2.App:
|
|
79
|
+
"""Set or clear an app's monthly transcoding spend cap.
|
|
80
|
+
|
|
81
|
+
Pass ``monthly_spend_limit_eur`` (must be > 0) to set the cap, or leave it
|
|
82
|
+
``None`` to clear the cap and return the app to unlimited. ``set_spend_limit``
|
|
83
|
+
and ``clear_spend_limit`` are the ergonomic shorthands.
|
|
84
|
+
"""
|
|
85
|
+
req = app_pb2.UpdateSpendLimitRequest(app_id=app_id)
|
|
86
|
+
if monthly_spend_limit_eur is not None:
|
|
87
|
+
# Setting the optional field marks presence; leaving it unset omits it
|
|
88
|
+
# from the request, which the server reads as "clear the cap".
|
|
89
|
+
req.monthly_spend_limit_eur = monthly_spend_limit_eur
|
|
90
|
+
return self._t.unary(
|
|
91
|
+
_SERVICE, "UpdateSpendLimit", req, app_pb2.UpdateSpendLimitResponse(), opts
|
|
92
|
+
).app
|
|
93
|
+
|
|
94
|
+
def set_spend_limit(
|
|
95
|
+
self, app_id: str, monthly_spend_limit_eur: float, opts: Optional[CallOptions] = None
|
|
96
|
+
) -> app_pb2.App:
|
|
97
|
+
"""Set the app's monthly transcoding spend cap in EUR (must be > 0).
|
|
98
|
+
|
|
99
|
+
Once recorded spend for the current billing period reaches the cap, new
|
|
100
|
+
jobs are rejected with the ``limit_exceeded`` error code; in-flight jobs
|
|
101
|
+
are never stopped. Use :meth:`clear_spend_limit` to return to unlimited.
|
|
102
|
+
"""
|
|
103
|
+
return self.update_spend_limit(app_id, monthly_spend_limit_eur, opts)
|
|
104
|
+
|
|
105
|
+
def clear_spend_limit(self, app_id: str, opts: Optional[CallOptions] = None) -> app_pb2.App:
|
|
106
|
+
"""Clear the app's monthly spend cap, returning it to unlimited (the default)."""
|
|
107
|
+
return self.update_spend_limit(app_id, None, opts)
|
|
108
|
+
|
|
109
|
+
def get_spend(
|
|
110
|
+
self, app_id: str, opts: Optional[CallOptions] = None
|
|
111
|
+
) -> app_pb2.GetSpendResponse:
|
|
112
|
+
"""Get the app's current-period transcoding spend against its limit.
|
|
113
|
+
|
|
114
|
+
Returns the billing-period bounds, EUR spent so far, the cap (if set), and
|
|
115
|
+
whether the 80% warning and 100% breach events have fired this period.
|
|
116
|
+
"""
|
|
117
|
+
req = app_pb2.GetSpendRequest(app_id=app_id)
|
|
118
|
+
return self._t.unary(_SERVICE, "GetSpend", req, app_pb2.GetSpendResponse(), opts)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Billing resource — an organization's invoices, read-only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .._transport.transport import CallOptions, Transport
|
|
8
|
+
from ..pagination import Page, PageContents
|
|
9
|
+
from ..v1 import billing_pb2
|
|
10
|
+
from ._helpers import assign_pagination
|
|
11
|
+
|
|
12
|
+
_SERVICE = "transcodely.v1.BillingService"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Billing:
|
|
16
|
+
"""An organization's billing statements.
|
|
17
|
+
|
|
18
|
+
Unlike every other resource, billing settles a whole organization rather
|
|
19
|
+
than a single app, so it is **not** available to API-key callers: a key is
|
|
20
|
+
scoped to one app, and there is no app-scoped subset of an invoice worth
|
|
21
|
+
serving. An API key gets a :class:`~transcodely.errors.PermissionError`.
|
|
22
|
+
|
|
23
|
+
Reading invoices needs a dashboard session token for an organization
|
|
24
|
+
**owner**, plus the organization the request is for::
|
|
25
|
+
|
|
26
|
+
client = Transcodely(session_token, organization_id="org_f6g7h8i9j0")
|
|
27
|
+
upcoming = client.billing.retrieve_upcoming()
|
|
28
|
+
|
|
29
|
+
Invoices are generated automatically when a period closes. There is no API
|
|
30
|
+
to create, edit, or delete one — a statement records what happened.
|
|
31
|
+
|
|
32
|
+
Monetary amounts are integer minor units (cents) of the invoice currency:
|
|
33
|
+
``total_cents == 1250`` in EUR is 12.50 EUR.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, transport: Transport) -> None:
|
|
37
|
+
self._t = transport
|
|
38
|
+
|
|
39
|
+
def list_invoices(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
limit: Optional[int] = None,
|
|
43
|
+
opts: Optional[CallOptions] = None,
|
|
44
|
+
) -> Page[billing_pb2.Invoice]:
|
|
45
|
+
"""Page through finalized invoices, newest period first.
|
|
46
|
+
|
|
47
|
+
Line items are omitted here — use :meth:`retrieve` for one invoice's
|
|
48
|
+
breakdown. A statement still being generated for a just-ended period is
|
|
49
|
+
never returned; for the period currently accruing use
|
|
50
|
+
:meth:`retrieve_upcoming`.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def fetch(cursor: Optional[str]) -> PageContents[billing_pb2.Invoice]:
|
|
54
|
+
req = billing_pb2.ListInvoicesRequest()
|
|
55
|
+
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
56
|
+
res = self._t.unary(
|
|
57
|
+
_SERVICE, "ListInvoices", req, billing_pb2.ListInvoicesResponse(), opts
|
|
58
|
+
)
|
|
59
|
+
return PageContents(
|
|
60
|
+
items=list(res.invoices),
|
|
61
|
+
next_cursor=res.pagination.next_cursor or None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return Page(fetch)
|
|
65
|
+
|
|
66
|
+
def retrieve(self, invoice_id: str, opts: Optional[CallOptions] = None) -> billing_pb2.Invoice:
|
|
67
|
+
"""Retrieve one invoice by ID (``inv_*``), including its line items."""
|
|
68
|
+
req = billing_pb2.GetInvoiceRequest(id=invoice_id)
|
|
69
|
+
return self._t.unary(
|
|
70
|
+
_SERVICE,
|
|
71
|
+
"GetInvoice",
|
|
72
|
+
req,
|
|
73
|
+
billing_pb2.GetInvoiceResponse(),
|
|
74
|
+
opts,
|
|
75
|
+
).invoice
|
|
76
|
+
|
|
77
|
+
def retrieve_upcoming(self, opts: Optional[CallOptions] = None) -> billing_pb2.Invoice:
|
|
78
|
+
"""Retrieve the statement for the period currently accruing.
|
|
79
|
+
|
|
80
|
+
Computed live from settled jobs rather than stored, so its ``id`` is
|
|
81
|
+
empty, its status is ``draft``, and its totals move as jobs finish. Jobs
|
|
82
|
+
still running are not included at any price — a job is billed only once
|
|
83
|
+
it settles.
|
|
84
|
+
"""
|
|
85
|
+
req = billing_pb2.GetUpcomingInvoiceRequest()
|
|
86
|
+
return self._t.unary(
|
|
87
|
+
_SERVICE,
|
|
88
|
+
"GetUpcomingInvoice",
|
|
89
|
+
req,
|
|
90
|
+
billing_pb2.GetUpcomingInvoiceResponse(),
|
|
91
|
+
opts,
|
|
92
|
+
).invoice
|