transcodely 0.3.0__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.0 → transcodely-0.3.2}/PKG-INFO +87 -4
- {transcodely-0.3.0 → transcodely-0.3.2}/README.md +85 -2
- {transcodely-0.3.0 → transcodely-0.3.2}/pyproject.toml +7 -2
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/__init__.py +20 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_transport/transport.py +4 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/client.py +16 -0
- transcodely-0.3.2/src/transcodely/resources/_helpers.py +66 -0
- transcodely-0.3.2/src/transcodely/resources/apps.py +118 -0
- transcodely-0.3.2/src/transcodely/resources/billing.py +92 -0
- transcodely-0.3.2/src/transcodely/resources/jobs.py +182 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/memberships.py +17 -2
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/users.py +15 -2
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/videos.py +39 -0
- {transcodely-0.3.0 → 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.0 → 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.0 → transcodely-0.3.2}/src/transcodely/v1/job_pb2.pyi +70 -8
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.py +13 -7
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.pyi +56 -3
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/thumbnails_pb2.py +14 -8
- {transcodely-0.3.0 → 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.0 → 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.0 → transcodely-0.3.2}/src/transcodely/v1/webhook_pb2.pyi +22 -6
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/version.py +1 -1
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/webhooks/types.py +28 -4
- transcodely-0.3.0/src/transcodely/resources/_helpers.py +0 -29
- transcodely-0.3.0/src/transcodely/resources/apps.py +0 -59
- transcodely-0.3.0/src/transcodely/resources/jobs.py +0 -120
- transcodely-0.3.0/src/transcodely/v1/app_pb2.py +0 -97
- transcodely-0.3.0/src/transcodely/v1/job_pb2.py +0 -205
- transcodely-0.3.0/src/transcodely/v1/video_pb2.py +0 -168
- transcodely-0.3.0/src/transcodely/v1/webhook_pb2.py +0 -151
- {transcodely-0.3.0 → transcodely-0.3.2}/.gitignore +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/LICENSE +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/buf/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/buf/validate/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/buf/validate/validate_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/buf/validate/validate_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_codec/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_codec/json_codec.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_transport/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_transport/error_mapping.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/_transport/headers.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/errors.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/pagination.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/py.typed +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/api_keys.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/events.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/health.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/organizations.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/origins.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/presets.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/resources/webhook_endpoints.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/streaming.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.pyi +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/webhooks/__init__.py +0 -0
- {transcodely-0.3.0 → transcodely-0.3.2}/src/transcodely/webhooks/construct_event.py +0 -0
- {transcodely-0.3.0 → 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)``."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Internal helpers shared by resource modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Optional, Union
|
|
6
|
+
|
|
7
|
+
from google.protobuf import json_format
|
|
8
|
+
from google.protobuf.descriptor import EnumDescriptor
|
|
9
|
+
from google.protobuf.message import Message
|
|
10
|
+
|
|
11
|
+
from .._codec.json_codec import expand_enum_value, simplify_enum_value, transform_enums_in_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fill_from_dict(msg: Message, data: Mapping[str, Any]) -> Message:
|
|
15
|
+
"""Populate ``msg`` from a plain dict, accepting simplified-enum strings.
|
|
16
|
+
|
|
17
|
+
The codec's enum-expand pass lets callers pass ``{"codec": "h264"}`` while
|
|
18
|
+
the proto stores ``VIDEO_CODEC_H264`` (an int after parse).
|
|
19
|
+
"""
|
|
20
|
+
payload = dict(data)
|
|
21
|
+
transform_enums_in_dict(payload, msg.DESCRIPTOR, mode="expand")
|
|
22
|
+
json_format.ParseDict(payload, msg, ignore_unknown_fields=True)
|
|
23
|
+
return msg
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_enum(value: Union[int, str], enum_desc: EnumDescriptor) -> int:
|
|
27
|
+
"""Resolve an enum value from either the raw proto int or a string.
|
|
28
|
+
|
|
29
|
+
Strings may be the simplified lowercase wire form (``"standard"``,
|
|
30
|
+
``"processing"``) or the canonical proto name (``"JOB_PRIORITY_STANDARD"``) —
|
|
31
|
+
both resolve to the same int via the codec's inbound ``expand`` rule. Ints
|
|
32
|
+
pass through unchanged (proto3 enums are open, so an unknown int is the
|
|
33
|
+
caller's responsibility). Used for scalar enum kwargs that are set on a
|
|
34
|
+
request directly rather than through :func:`fill_from_dict`.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(value, bool): # bool is an int subclass — reject it explicitly.
|
|
37
|
+
raise TypeError(f"{enum_desc.name} value must be an int or str, got bool: {value!r}")
|
|
38
|
+
if isinstance(value, int):
|
|
39
|
+
return value
|
|
40
|
+
if isinstance(value, str):
|
|
41
|
+
name = expand_enum_value(value, enum_desc)
|
|
42
|
+
entry = enum_desc.values_by_name.get(name)
|
|
43
|
+
if entry is None:
|
|
44
|
+
allowed = ", ".join(
|
|
45
|
+
simplify_enum_value(v.name, enum_desc) for v in enum_desc.values if v.number != 0
|
|
46
|
+
)
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"unknown {enum_desc.name} value {value!r}; expected one of: {allowed}"
|
|
49
|
+
)
|
|
50
|
+
return int(entry.number)
|
|
51
|
+
raise TypeError(f"{enum_desc.name} value must be an int or str, got {type(value).__name__}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def assign_pagination(
|
|
55
|
+
req_pagination: Any,
|
|
56
|
+
*,
|
|
57
|
+
limit: Optional[int] = None,
|
|
58
|
+
cursor: Optional[str] = None,
|
|
59
|
+
offset: Optional[int] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if limit is not None:
|
|
62
|
+
req_pagination.limit = limit
|
|
63
|
+
if cursor is not None:
|
|
64
|
+
req_pagination.cursor = cursor
|
|
65
|
+
if offset is not None:
|
|
66
|
+
req_pagination.offset = offset
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""App resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from .._transport.transport import CallOptions, Transport
|
|
8
|
+
from ..pagination import Page, PageContents
|
|
9
|
+
from ..v1 import app_pb2
|
|
10
|
+
from ._helpers import assign_pagination, fill_from_dict
|
|
11
|
+
|
|
12
|
+
_SERVICE = "transcodely.v1.AppService"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Apps:
|
|
16
|
+
def __init__(self, transport: Transport) -> None:
|
|
17
|
+
self._t = transport
|
|
18
|
+
|
|
19
|
+
def create(self, **kwargs: Any) -> app_pb2.App:
|
|
20
|
+
req = fill_from_dict(app_pb2.CreateAppRequest(), kwargs)
|
|
21
|
+
return self._t.unary(_SERVICE, "Create", req, app_pb2.CreateAppResponse()).app
|
|
22
|
+
|
|
23
|
+
def get(self, id: str, opts: Optional[CallOptions] = None) -> app_pb2.App:
|
|
24
|
+
req = app_pb2.GetAppRequest(id=id)
|
|
25
|
+
return self._t.unary(_SERVICE, "Get", req, app_pb2.GetAppResponse(), opts).app
|
|
26
|
+
|
|
27
|
+
def update(self, **kwargs: Any) -> app_pb2.App:
|
|
28
|
+
req = fill_from_dict(app_pb2.UpdateAppRequest(), kwargs)
|
|
29
|
+
return self._t.unary(_SERVICE, "Update", req, app_pb2.UpdateAppResponse()).app
|
|
30
|
+
|
|
31
|
+
def list(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
org_id: Optional[str] = None,
|
|
35
|
+
limit: Optional[int] = None,
|
|
36
|
+
include_archived: Optional[bool] = None,
|
|
37
|
+
opts: Optional[CallOptions] = None,
|
|
38
|
+
) -> Page[app_pb2.App]:
|
|
39
|
+
"""List apps in an organization.
|
|
40
|
+
|
|
41
|
+
``org_id`` (the parent organization, e.g. ``"org_a1b2c3d4e5"``) is required
|
|
42
|
+
by the API. Pass ``include_archived=True`` to include archived apps.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def fetch(cursor: Optional[str]) -> PageContents[app_pb2.App]:
|
|
46
|
+
req = app_pb2.ListAppsRequest()
|
|
47
|
+
if org_id is not None:
|
|
48
|
+
req.org_id = org_id
|
|
49
|
+
if include_archived is not None:
|
|
50
|
+
req.include_archived = include_archived
|
|
51
|
+
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
52
|
+
res = self._t.unary(_SERVICE, "List", req, app_pb2.ListAppsResponse(), opts)
|
|
53
|
+
return PageContents(
|
|
54
|
+
items=list(res.apps), next_cursor=res.pagination.next_cursor or None
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return Page(fetch)
|
|
58
|
+
|
|
59
|
+
def archive(self, id: str, opts: Optional[CallOptions] = None) -> app_pb2.App:
|
|
60
|
+
req = app_pb2.ArchiveAppRequest(id=id)
|
|
61
|
+
return self._t.unary(_SERVICE, "Archive", req, app_pb2.ArchiveAppResponse(), opts).app
|
|
62
|
+
|
|
63
|
+
def enable_hosting(self, **kwargs: Any) -> app_pb2.EnableHostingResponse:
|
|
64
|
+
req = fill_from_dict(app_pb2.EnableHostingRequest(), kwargs)
|
|
65
|
+
return self._t.unary(_SERVICE, "EnableHosting", req, app_pb2.EnableHostingResponse())
|
|
66
|
+
|
|
67
|
+
def update_hosting_config(self, **kwargs: Any) -> app_pb2.UpdateHostingConfigResponse:
|
|
68
|
+
req = fill_from_dict(app_pb2.UpdateHostingConfigRequest(), kwargs)
|
|
69
|
+
return self._t.unary(
|
|
70
|
+
_SERVICE, "UpdateHostingConfig", req, app_pb2.UpdateHostingConfigResponse()
|
|
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)
|