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.
Files changed (96) hide show
  1. {transcodely-0.3.1 → transcodely-0.3.2}/PKG-INFO +87 -4
  2. {transcodely-0.3.1 → transcodely-0.3.2}/README.md +85 -2
  3. {transcodely-0.3.1 → transcodely-0.3.2}/pyproject.toml +7 -2
  4. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/__init__.py +20 -0
  5. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/transport.py +4 -0
  6. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/client.py +16 -0
  7. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/apps.py +47 -0
  8. transcodely-0.3.2/src/transcodely/resources/billing.py +92 -0
  9. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/jobs.py +33 -6
  10. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/memberships.py +3 -2
  11. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/users.py +3 -2
  12. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/videos.py +39 -0
  13. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/types.py +86 -0
  14. transcodely-0.3.2/src/transcodely/v1/app_pb2.py +111 -0
  15. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/app_pb2.pyi +48 -4
  16. transcodely-0.3.2/src/transcodely/v1/billing_pb2.py +65 -0
  17. transcodely-0.3.2/src/transcodely/v1/billing_pb2.pyi +131 -0
  18. transcodely-0.3.2/src/transcodely/v1/job_pb2.py +226 -0
  19. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/job_pb2.pyi +70 -8
  20. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.py +13 -7
  21. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/subtitles_pb2.pyi +56 -3
  22. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/thumbnails_pb2.py +14 -8
  23. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/thumbnails_pb2.pyi +10 -2
  24. transcodely-0.3.2/src/transcodely/v1/video_pb2.py +210 -0
  25. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/video_pb2.pyi +168 -10
  26. transcodely-0.3.2/src/transcodely/v1/watermark_pb2.py +57 -0
  27. transcodely-0.3.2/src/transcodely/v1/watermark_pb2.pyi +56 -0
  28. transcodely-0.3.2/src/transcodely/v1/webhook_pb2.py +155 -0
  29. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/webhook_pb2.pyi +22 -6
  30. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/version.py +1 -1
  31. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/types.py +28 -4
  32. transcodely-0.3.1/src/transcodely/v1/app_pb2.py +0 -97
  33. transcodely-0.3.1/src/transcodely/v1/job_pb2.py +0 -205
  34. transcodely-0.3.1/src/transcodely/v1/video_pb2.py +0 -168
  35. transcodely-0.3.1/src/transcodely/v1/webhook_pb2.py +0 -151
  36. {transcodely-0.3.1 → transcodely-0.3.2}/.gitignore +0 -0
  37. {transcodely-0.3.1 → transcodely-0.3.2}/LICENSE +0 -0
  38. {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/__init__.py +0 -0
  39. {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/__init__.py +0 -0
  40. {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/validate_pb2.py +0 -0
  41. {transcodely-0.3.1 → transcodely-0.3.2}/src/buf/validate/validate_pb2.pyi +0 -0
  42. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_codec/__init__.py +0 -0
  43. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_codec/json_codec.py +0 -0
  44. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/__init__.py +0 -0
  45. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/error_mapping.py +0 -0
  46. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/_transport/headers.py +0 -0
  47. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/errors.py +0 -0
  48. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/pagination.py +0 -0
  49. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/py.typed +0 -0
  50. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/__init__.py +0 -0
  51. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/_helpers.py +0 -0
  52. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/api_keys.py +0 -0
  53. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/events.py +0 -0
  54. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/health.py +0 -0
  55. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/organizations.py +0 -0
  56. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/origins.py +0 -0
  57. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/presets.py +0 -0
  58. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/resources/webhook_endpoints.py +0 -0
  59. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/streaming.py +0 -0
  60. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.py +0 -0
  61. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/api_key_pb2.pyi +0 -0
  62. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.py +0 -0
  63. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_av1_pb2.pyi +0 -0
  64. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.py +0 -0
  65. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h264_pb2.pyi +0 -0
  66. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.py +0 -0
  67. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_h265_pb2.pyi +0 -0
  68. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.py +0 -0
  69. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/codec_vp9_pb2.pyi +0 -0
  70. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.py +0 -0
  71. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/common_pb2.pyi +0 -0
  72. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.py +0 -0
  73. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/content_aware_pb2.pyi +0 -0
  74. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.py +0 -0
  75. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/drm_pb2.pyi +0 -0
  76. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.py +0 -0
  77. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/hdr_pb2.pyi +0 -0
  78. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.py +0 -0
  79. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/health_pb2.pyi +0 -0
  80. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.py +0 -0
  81. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/media_pb2.pyi +0 -0
  82. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.py +0 -0
  83. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/membership_pb2.pyi +0 -0
  84. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.py +0 -0
  85. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/organization_pb2.pyi +0 -0
  86. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.py +0 -0
  87. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/origin_pb2.pyi +0 -0
  88. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.py +0 -0
  89. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/preset_pb2.pyi +0 -0
  90. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.py +0 -0
  91. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/streaming_pb2.pyi +0 -0
  92. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.py +0 -0
  93. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/v1/user_pb2.pyi +0 -0
  94. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/__init__.py +0 -0
  95. {transcodely-0.3.1 → transcodely-0.3.2}/src/transcodely/webhooks/construct_event.py +0 -0
  96. {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.1
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>=0.5; extra == 'dev'
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 13 event types are `job.created`, `job.succeeded`, `job.failed`, `job.canceled`, `job.progress`, `output.created`, `output.ready`, `output.failed`, `output.progress`, `video.uploaded`, `video.deleted`, `app.created`, and `app.updated`. 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`.
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 13 event types are `job.created`, `job.succeeded`, `job.failed`, `job.canceled`, `job.progress`, `output.created`, `output.ready`, `output.failed`, `output.progress`, `video.uploaded`, `video.deleted`, `app.created`, and `app.updated`. 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`.
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.1"
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
- "ruff>=0.5",
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