transcodely 0.3.8__tar.gz → 0.3.10__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.8 → transcodely-0.3.10}/PKG-INFO +130 -6
- {transcodely-0.3.8 → transcodely-0.3.10}/README.md +125 -1
- {transcodely-0.3.8 → transcodely-0.3.10}/pyproject.toml +7 -5
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/__init__.py +8 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/client.py +9 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/events.py +12 -2
- transcodely-0.3.10/src/transcodely/resources/ingest_rules.py +183 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/webhook_endpoints.py +12 -2
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/types.py +80 -5
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/api_key_pb2.py +19 -19
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/content_aware_pb2.py +2 -2
- transcodely-0.3.10/src/transcodely/v1/ingest_rule_pb2.py +157 -0
- transcodely-0.3.10/src/transcodely/v1/ingest_rule_pb2.pyi +305 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/job_pb2.py +74 -58
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/job_pb2.pyi +118 -2
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/video_pb2.py +4 -4
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/webhook_pb2.py +4 -4
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/version.py +1 -1
- {transcodely-0.3.8 → transcodely-0.3.10}/.gitignore +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/LICENSE +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/buf/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/buf/validate/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/buf/validate/validate_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/buf/validate/validate_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_codec/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_codec/json_codec.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_transport/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_transport/error_mapping.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_transport/headers.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/_transport/transport.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/errors.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/pagination.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/py.typed +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/_helpers.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/api_keys.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/apps.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/billing.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/health.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/jobs.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/memberships.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/organizations.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/origins.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/presets.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/users.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/resources/videos.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/streaming.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/api_key_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/app_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/app_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/billing_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/billing_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_av1_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_av1_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_h264_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_h264_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_h265_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_h265_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_vp9_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/codec_vp9_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/common_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/common_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/content_aware_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/drm_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/drm_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/hdr_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/hdr_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/health_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/health_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/media_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/media_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/membership_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/membership_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/organization_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/organization_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/origin_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/origin_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/preset_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/preset_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/streaming_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/streaming_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/subtitles_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/subtitles_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/thumbnails_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/thumbnails_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/user_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/user_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/video_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/watermark_pb2.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/watermark_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/v1/webhook_pb2.pyi +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/webhooks/__init__.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/webhooks/construct_event.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/webhooks/signature.py +0 -0
- {transcodely-0.3.8 → transcodely-0.3.10}/src/transcodely/webhooks/types.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: transcodely
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Official Python SDK for
|
|
5
|
-
Project-URL: Homepage, https://transcodely.com
|
|
6
|
-
Project-URL: Documentation, https://transcodely.com/docs
|
|
3
|
+
Version: 0.3.10
|
|
4
|
+
Summary: Official Python SDK for Transcodely — encode video to HLS, DASH and MP4 into your own S3, GCS or R2 bucket, with DRM and signed playback.
|
|
5
|
+
Project-URL: Homepage, https://www.transcodely.com
|
|
6
|
+
Project-URL: Documentation, https://www.transcodely.com/docs
|
|
7
7
|
Project-URL: Repository, https://github.com/transcodely/transcodely-python
|
|
8
8
|
Project-URL: Issues, https://github.com/transcodely/transcodely-python/issues
|
|
9
9
|
Author: Transcodely
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
12
|
-
Keywords: connect-rpc,dash,encoding,hls,streaming,transcodely,transcoding,video
|
|
12
|
+
Keywords: abr,cmaf,connect-rpc,dash,drm,encoding,fairplay,ffmpeg,hls,r2,s3,signed-urls,streaming,transcodely,transcoding,video,video-hosting,widevine
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -37,7 +37,9 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
# transcodely
|
|
39
39
|
|
|
40
|
-
Official Python SDK for
|
|
40
|
+
Official Python SDK for [Transcodely](https://www.transcodely.com) — encode video into
|
|
41
|
+
HLS, DASH and MP4 and write it to your own S3, GCS or R2 bucket, with DRM, signed
|
|
42
|
+
playback and deterministic output paths. Or let Transcodely host and deliver it.
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
pip install transcodely
|
|
@@ -75,6 +77,61 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
75
77
|
|
|
76
78
|
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.
|
|
77
79
|
|
|
80
|
+
## Read the output report
|
|
81
|
+
|
|
82
|
+
Every completed output carries a report of what the produced file actually
|
|
83
|
+
turned out to be — measured from the encoded file rather than copied from the
|
|
84
|
+
request — plus the verdict of comparing those measurements against what was
|
|
85
|
+
asked for.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from transcodely.types import OutputReport
|
|
89
|
+
|
|
90
|
+
done = client.jobs.get(job.id)
|
|
91
|
+
|
|
92
|
+
for output in done.outputs:
|
|
93
|
+
if not output.HasField("report"):
|
|
94
|
+
continue # not measured — never "nothing wrong"
|
|
95
|
+
|
|
96
|
+
report: OutputReport = output.report
|
|
97
|
+
print(
|
|
98
|
+
output.id,
|
|
99
|
+
report.video.codec,
|
|
100
|
+
f"{report.video.width}x{report.video.height}",
|
|
101
|
+
f"{report.duration_seconds}s",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not report.verdict.matches_request:
|
|
105
|
+
for m in report.verdict.mismatches:
|
|
106
|
+
print(f" {m.field}: asked for {m.expected}, got {m.actual}")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Check `output.HasField("report")` rather than truthiness: an absent report means
|
|
110
|
+
"not measured", which is not the same as a report that found nothing wrong.
|
|
111
|
+
Branch on `m.field` — it comes from a fixed vocabulary (`video.codec`,
|
|
112
|
+
`video.resolution`, `duration_seconds`, …) — rather than on the values beside
|
|
113
|
+
it. For an ABR ladder the facts describe the highest-resolution rendition, the
|
|
114
|
+
same one the verdict judges; per-rendition detail stays in `variant_results`.
|
|
115
|
+
|
|
116
|
+
An output encoded with per-title content-aware analysis also carries
|
|
117
|
+
`report.content_aware`: the VMAF target the search aimed at, the score it reached
|
|
118
|
+
on its samples, the CRF it chose, and — since API 5.23.0 — the whole curve the
|
|
119
|
+
search measured to get there: `seed_crf` (what the rung would have used without
|
|
120
|
+
the search), `met_target` (whether any probe reached the VMAF target), and
|
|
121
|
+
`probes` (every sample point, in order, each with `crf`, `vmaf`, and
|
|
122
|
+
`bitrate_kbps`). It describes the SEARCH, not the delivered file — every number
|
|
123
|
+
is measured on short samples cut from the source, and the delivered file is
|
|
124
|
+
never itself scored. Check `report.HasField("content_aware")`; an ordinary
|
|
125
|
+
output has none. `probes` and `met_target` are empty/absent for workers older
|
|
126
|
+
than 1.29.0, which ran the search but didn't report its curve.
|
|
127
|
+
|
|
128
|
+
A probe's `bitrate_kbps` is derived from a sample encoded **video-only** — the
|
|
129
|
+
search's cuts drop audio, subtitles and data — so it's the sample's video
|
|
130
|
+
bitrate, not a muxed file's rate. The only defensible saving is the ratio
|
|
131
|
+
between the `seed_crf` probe's bitrate and the `crf_chosen` probe's: same cut,
|
|
132
|
+
same settings, only the CRF differs. Comparing a probe's bitrate against the
|
|
133
|
+
delivered output's `average_bitrate_kbps` compares two different things.
|
|
134
|
+
|
|
78
135
|
## AI captions
|
|
79
136
|
|
|
80
137
|
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`.
|
|
@@ -119,6 +176,7 @@ client.jobs # create / get / list / cancel / confirm / watch
|
|
|
119
176
|
client.videos # upload helpers, multipart, create_from_url, get / list / update / delete / watch, get_stats / list_top_videos
|
|
120
177
|
client.presets # create / get / get_by_slug / list / update / duplicate / archive
|
|
121
178
|
client.origins # create / get / list / update / validate / archive
|
|
179
|
+
client.ingest_rules # create / get / list / update / delete / list_events / test / replay_event
|
|
122
180
|
client.apps # create / get / list / update / archive / enable_hosting
|
|
123
181
|
client.api_keys # create / get / list / revoke
|
|
124
182
|
client.organizations # create / get / list / update / check_slug
|
|
@@ -269,6 +327,72 @@ r2 = {
|
|
|
269
327
|
|
|
270
328
|
Provide either `account_id` or `endpoint`, never both. `jurisdiction` only applies when `account_id` is set.
|
|
271
329
|
|
|
330
|
+
## Ingest rules
|
|
331
|
+
|
|
332
|
+
An ingest rule is a standing instruction on one readable origin: *when an object
|
|
333
|
+
matching these filters lands, create this job for it*. Your storage provider
|
|
334
|
+
posts its object-created events to the rule's endpoint, and no server of yours
|
|
335
|
+
is in the path. Amazon S3 via SNS, Google Cloud Storage via a Pub/Sub push
|
|
336
|
+
subscription, Supabase Storage via a database webhook, and a generic shape for
|
|
337
|
+
anything else are all recognised from the payload.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
created = client.ingest_rules.create(
|
|
341
|
+
origin_id="ori_a1b2c3d4e5f6",
|
|
342
|
+
name="Watch uploads/",
|
|
343
|
+
filters={
|
|
344
|
+
"prefix": "uploads/",
|
|
345
|
+
"suffixes": [".mp4", ".mov"],
|
|
346
|
+
"min_bytes": 1024, # ignore the zero-byte placeholder some clients write first
|
|
347
|
+
},
|
|
348
|
+
action={
|
|
349
|
+
"outputs": [{"preset": "web_1080p_standard"}],
|
|
350
|
+
"managed": True, # host and deliver the result; use output_origin_id for your own bucket
|
|
351
|
+
"priority": "standard",
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
print("point your bucket notifications at", created.rule.endpoint_url)
|
|
356
|
+
print("secret (shown once):", created.secret)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`created.secret` is the **only** time the inbound secret is readable — store it
|
|
360
|
+
wherever the event sender will read it from. A later `get` returns just
|
|
361
|
+
`secret_prefix` and `secret_hint`. Lost it? `update` with `rotate_secret=True`
|
|
362
|
+
issues a new one, and the previous one keeps working for 24 hours so the sender
|
|
363
|
+
can be changed without dropping an event.
|
|
364
|
+
|
|
365
|
+
Every delivery is recorded, whether or not it became a job:
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
for event in client.ingest_rules.list_events(rule_id=created.rule.id).auto_paging_iter():
|
|
369
|
+
print(event.id, event.object_key, event.status, event.reason)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
A `skipped` event names why in `reason` — `filter_prefix`, `filter_suffix`,
|
|
373
|
+
`filter_content_type`, `filter_size`, `bucket_mismatch`, `rule_disabled`, or
|
|
374
|
+
`duplicate`. A `failed` one carries the API error code that refused the job,
|
|
375
|
+
such as `limit_exceeded`. Narrow the log to either with the simplified status
|
|
376
|
+
string: `list_events(status="skipped")`.
|
|
377
|
+
|
|
378
|
+
Deduplication is permanent: an object is identified by (rule, bucket, key,
|
|
379
|
+
etag), so re-sending the event or re-uploading the same bytes produces nothing.
|
|
380
|
+
To give an object another pass — one that arrived while the rule was paused, or
|
|
381
|
+
was refused while the account was over its cap — replay it:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
replayed = client.ingest_rules.replay_event("sev_a1b2c3d4e5f6g7")
|
|
385
|
+
print(replayed.id, "is back in", StorageEventStatus.Name(replayed.status))
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Only `skipped` and `failed` events can be replayed. When an `update` switches a
|
|
389
|
+
paused rule back on, the response reports how large that backlog is in
|
|
390
|
+
`events_skipped_while_disabled`.
|
|
391
|
+
|
|
392
|
+
Before wiring the provider up, dry-run a key against the rule with
|
|
393
|
+
`client.ingest_rules.test(...)`: it reports whether the filters match and, when
|
|
394
|
+
they do, the exact job request the rule would submit. Nothing is stored.
|
|
395
|
+
|
|
272
396
|
## Billing
|
|
273
397
|
|
|
274
398
|
Billing settles a whole organization, so `client.billing` needs a dashboard session token for an
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# transcodely
|
|
2
2
|
|
|
3
|
-
Official Python SDK for
|
|
3
|
+
Official Python SDK for [Transcodely](https://www.transcodely.com) — encode video into
|
|
4
|
+
HLS, DASH and MP4 and write it to your own S3, GCS or R2 bucket, with DRM, signed
|
|
5
|
+
playback and deterministic output paths. Or let Transcodely host and deliver it.
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
8
|
pip install transcodely
|
|
@@ -38,6 +40,61 @@ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
|
|
|
38
40
|
|
|
39
41
|
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.
|
|
40
42
|
|
|
43
|
+
## Read the output report
|
|
44
|
+
|
|
45
|
+
Every completed output carries a report of what the produced file actually
|
|
46
|
+
turned out to be — measured from the encoded file rather than copied from the
|
|
47
|
+
request — plus the verdict of comparing those measurements against what was
|
|
48
|
+
asked for.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from transcodely.types import OutputReport
|
|
52
|
+
|
|
53
|
+
done = client.jobs.get(job.id)
|
|
54
|
+
|
|
55
|
+
for output in done.outputs:
|
|
56
|
+
if not output.HasField("report"):
|
|
57
|
+
continue # not measured — never "nothing wrong"
|
|
58
|
+
|
|
59
|
+
report: OutputReport = output.report
|
|
60
|
+
print(
|
|
61
|
+
output.id,
|
|
62
|
+
report.video.codec,
|
|
63
|
+
f"{report.video.width}x{report.video.height}",
|
|
64
|
+
f"{report.duration_seconds}s",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not report.verdict.matches_request:
|
|
68
|
+
for m in report.verdict.mismatches:
|
|
69
|
+
print(f" {m.field}: asked for {m.expected}, got {m.actual}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Check `output.HasField("report")` rather than truthiness: an absent report means
|
|
73
|
+
"not measured", which is not the same as a report that found nothing wrong.
|
|
74
|
+
Branch on `m.field` — it comes from a fixed vocabulary (`video.codec`,
|
|
75
|
+
`video.resolution`, `duration_seconds`, …) — rather than on the values beside
|
|
76
|
+
it. For an ABR ladder the facts describe the highest-resolution rendition, the
|
|
77
|
+
same one the verdict judges; per-rendition detail stays in `variant_results`.
|
|
78
|
+
|
|
79
|
+
An output encoded with per-title content-aware analysis also carries
|
|
80
|
+
`report.content_aware`: the VMAF target the search aimed at, the score it reached
|
|
81
|
+
on its samples, the CRF it chose, and — since API 5.23.0 — the whole curve the
|
|
82
|
+
search measured to get there: `seed_crf` (what the rung would have used without
|
|
83
|
+
the search), `met_target` (whether any probe reached the VMAF target), and
|
|
84
|
+
`probes` (every sample point, in order, each with `crf`, `vmaf`, and
|
|
85
|
+
`bitrate_kbps`). It describes the SEARCH, not the delivered file — every number
|
|
86
|
+
is measured on short samples cut from the source, and the delivered file is
|
|
87
|
+
never itself scored. Check `report.HasField("content_aware")`; an ordinary
|
|
88
|
+
output has none. `probes` and `met_target` are empty/absent for workers older
|
|
89
|
+
than 1.29.0, which ran the search but didn't report its curve.
|
|
90
|
+
|
|
91
|
+
A probe's `bitrate_kbps` is derived from a sample encoded **video-only** — the
|
|
92
|
+
search's cuts drop audio, subtitles and data — so it's the sample's video
|
|
93
|
+
bitrate, not a muxed file's rate. The only defensible saving is the ratio
|
|
94
|
+
between the `seed_crf` probe's bitrate and the `crf_chosen` probe's: same cut,
|
|
95
|
+
same settings, only the CRF differs. Comparing a probe's bitrate against the
|
|
96
|
+
delivered output's `average_bitrate_kbps` compares two different things.
|
|
97
|
+
|
|
41
98
|
## AI captions
|
|
42
99
|
|
|
43
100
|
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`.
|
|
@@ -82,6 +139,7 @@ client.jobs # create / get / list / cancel / confirm / watch
|
|
|
82
139
|
client.videos # upload helpers, multipart, create_from_url, get / list / update / delete / watch, get_stats / list_top_videos
|
|
83
140
|
client.presets # create / get / get_by_slug / list / update / duplicate / archive
|
|
84
141
|
client.origins # create / get / list / update / validate / archive
|
|
142
|
+
client.ingest_rules # create / get / list / update / delete / list_events / test / replay_event
|
|
85
143
|
client.apps # create / get / list / update / archive / enable_hosting
|
|
86
144
|
client.api_keys # create / get / list / revoke
|
|
87
145
|
client.organizations # create / get / list / update / check_slug
|
|
@@ -232,6 +290,72 @@ r2 = {
|
|
|
232
290
|
|
|
233
291
|
Provide either `account_id` or `endpoint`, never both. `jurisdiction` only applies when `account_id` is set.
|
|
234
292
|
|
|
293
|
+
## Ingest rules
|
|
294
|
+
|
|
295
|
+
An ingest rule is a standing instruction on one readable origin: *when an object
|
|
296
|
+
matching these filters lands, create this job for it*. Your storage provider
|
|
297
|
+
posts its object-created events to the rule's endpoint, and no server of yours
|
|
298
|
+
is in the path. Amazon S3 via SNS, Google Cloud Storage via a Pub/Sub push
|
|
299
|
+
subscription, Supabase Storage via a database webhook, and a generic shape for
|
|
300
|
+
anything else are all recognised from the payload.
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
created = client.ingest_rules.create(
|
|
304
|
+
origin_id="ori_a1b2c3d4e5f6",
|
|
305
|
+
name="Watch uploads/",
|
|
306
|
+
filters={
|
|
307
|
+
"prefix": "uploads/",
|
|
308
|
+
"suffixes": [".mp4", ".mov"],
|
|
309
|
+
"min_bytes": 1024, # ignore the zero-byte placeholder some clients write first
|
|
310
|
+
},
|
|
311
|
+
action={
|
|
312
|
+
"outputs": [{"preset": "web_1080p_standard"}],
|
|
313
|
+
"managed": True, # host and deliver the result; use output_origin_id for your own bucket
|
|
314
|
+
"priority": "standard",
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
print("point your bucket notifications at", created.rule.endpoint_url)
|
|
319
|
+
print("secret (shown once):", created.secret)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`created.secret` is the **only** time the inbound secret is readable — store it
|
|
323
|
+
wherever the event sender will read it from. A later `get` returns just
|
|
324
|
+
`secret_prefix` and `secret_hint`. Lost it? `update` with `rotate_secret=True`
|
|
325
|
+
issues a new one, and the previous one keeps working for 24 hours so the sender
|
|
326
|
+
can be changed without dropping an event.
|
|
327
|
+
|
|
328
|
+
Every delivery is recorded, whether or not it became a job:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
for event in client.ingest_rules.list_events(rule_id=created.rule.id).auto_paging_iter():
|
|
332
|
+
print(event.id, event.object_key, event.status, event.reason)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
A `skipped` event names why in `reason` — `filter_prefix`, `filter_suffix`,
|
|
336
|
+
`filter_content_type`, `filter_size`, `bucket_mismatch`, `rule_disabled`, or
|
|
337
|
+
`duplicate`. A `failed` one carries the API error code that refused the job,
|
|
338
|
+
such as `limit_exceeded`. Narrow the log to either with the simplified status
|
|
339
|
+
string: `list_events(status="skipped")`.
|
|
340
|
+
|
|
341
|
+
Deduplication is permanent: an object is identified by (rule, bucket, key,
|
|
342
|
+
etag), so re-sending the event or re-uploading the same bytes produces nothing.
|
|
343
|
+
To give an object another pass — one that arrived while the rule was paused, or
|
|
344
|
+
was refused while the account was over its cap — replay it:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
replayed = client.ingest_rules.replay_event("sev_a1b2c3d4e5f6g7")
|
|
348
|
+
print(replayed.id, "is back in", StorageEventStatus.Name(replayed.status))
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Only `skipped` and `failed` events can be replayed. When an `update` switches a
|
|
352
|
+
paused rule back on, the response reports how large that backlog is in
|
|
353
|
+
`events_skipped_while_disabled`.
|
|
354
|
+
|
|
355
|
+
Before wiring the provider up, dry-run a key against the rule with
|
|
356
|
+
`client.ingest_rules.test(...)`: it reports whether the filters match and, when
|
|
357
|
+
they do, the exact job request the rule would submit. Nothing is stored.
|
|
358
|
+
|
|
235
359
|
## Billing
|
|
236
360
|
|
|
237
361
|
Billing settles a whole organization, so `client.billing` needs a dashboard session token for an
|
|
@@ -4,14 +4,16 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "transcodely"
|
|
7
|
-
version = "0.3.
|
|
8
|
-
description = "Official Python SDK for
|
|
7
|
+
version = "0.3.10"
|
|
8
|
+
description = "Official Python SDK for Transcodely — encode video to HLS, DASH and MP4 into your own S3, GCS or R2 bucket, with DRM and signed playback."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
12
|
authors = [{ name = "Transcodely" }]
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
|
-
keywords = ["transcodely", "video", "transcoding", "encoding", "hls", "dash", "
|
|
14
|
+
keywords = ["transcodely", "video", "transcoding", "encoding", "hls", "dash", "cmaf",
|
|
15
|
+
"drm", "widevine", "fairplay", "abr", "ffmpeg", "s3", "r2", "signed-urls",
|
|
16
|
+
"video-hosting", "streaming", "connect-rpc"]
|
|
15
17
|
classifiers = [
|
|
16
18
|
"Development Status :: 3 - Alpha",
|
|
17
19
|
"Intended Audience :: Developers",
|
|
@@ -42,8 +44,8 @@ dev = [
|
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
[project.urls]
|
|
45
|
-
Homepage = "https://transcodely.com"
|
|
46
|
-
Documentation = "https://transcodely.com/docs"
|
|
47
|
+
Homepage = "https://www.transcodely.com"
|
|
48
|
+
Documentation = "https://www.transcodely.com/docs"
|
|
47
49
|
Repository = "https://github.com/transcodely/transcodely-python"
|
|
48
50
|
Issues = "https://github.com/transcodely/transcodely-python/issues"
|
|
49
51
|
|
|
@@ -67,6 +67,7 @@ from .types import (
|
|
|
67
67
|
DRMConfig,
|
|
68
68
|
HDRConfig,
|
|
69
69
|
HLSConfig,
|
|
70
|
+
IngestRule,
|
|
70
71
|
Invoice,
|
|
71
72
|
InvoiceLineItem,
|
|
72
73
|
InvoiceLineType,
|
|
@@ -85,6 +86,9 @@ from .types import (
|
|
|
85
86
|
Preset,
|
|
86
87
|
PresetVariant,
|
|
87
88
|
Resolution,
|
|
89
|
+
StorageEvent,
|
|
90
|
+
StorageEventSource,
|
|
91
|
+
StorageEventStatus,
|
|
88
92
|
StreamingConfig,
|
|
89
93
|
SubtitleResult,
|
|
90
94
|
SubtitleTrack,
|
|
@@ -149,6 +153,7 @@ __all__ = [
|
|
|
149
153
|
"FieldViolation",
|
|
150
154
|
"HDRConfig",
|
|
151
155
|
"HLSConfig",
|
|
156
|
+
"IngestRule",
|
|
152
157
|
"InvalidRequestError",
|
|
153
158
|
"Invoice",
|
|
154
159
|
"InvoiceLineItem",
|
|
@@ -174,6 +179,9 @@ __all__ = [
|
|
|
174
179
|
"PresetVariant",
|
|
175
180
|
"RateLimitError",
|
|
176
181
|
"Resolution",
|
|
182
|
+
"StorageEvent",
|
|
183
|
+
"StorageEventSource",
|
|
184
|
+
"StorageEventStatus",
|
|
177
185
|
"StreamingConfig",
|
|
178
186
|
"SubtitleResult",
|
|
179
187
|
"SubtitleTrack",
|
|
@@ -13,6 +13,7 @@ from .resources.apps import Apps
|
|
|
13
13
|
from .resources.billing import Billing
|
|
14
14
|
from .resources.events import Events
|
|
15
15
|
from .resources.health import Health
|
|
16
|
+
from .resources.ingest_rules import IngestRules
|
|
16
17
|
from .resources.jobs import Jobs
|
|
17
18
|
from .resources.memberships import Memberships
|
|
18
19
|
from .resources.organizations import Organizations
|
|
@@ -63,6 +64,7 @@ class Transcodely:
|
|
|
63
64
|
self._videos: Videos | None = None
|
|
64
65
|
self._presets: Presets | None = None
|
|
65
66
|
self._origins: Origins | None = None
|
|
67
|
+
self._ingest_rules: IngestRules | None = None
|
|
66
68
|
self._apps: Apps | None = None
|
|
67
69
|
self._api_keys: ApiKeys | None = None
|
|
68
70
|
self._organizations: Organizations | None = None
|
|
@@ -111,6 +113,13 @@ class Transcodely:
|
|
|
111
113
|
self._origins = Origins(self._transport)
|
|
112
114
|
return self._origins
|
|
113
115
|
|
|
116
|
+
@property
|
|
117
|
+
def ingest_rules(self) -> IngestRules:
|
|
118
|
+
"""Ingest rules: watch a bucket, transcode what lands in it."""
|
|
119
|
+
if self._ingest_rules is None:
|
|
120
|
+
self._ingest_rules = IngestRules(self._transport)
|
|
121
|
+
return self._ingest_rules
|
|
122
|
+
|
|
114
123
|
@property
|
|
115
124
|
def apps(self) -> Apps:
|
|
116
125
|
if self._apps is None:
|
|
@@ -52,15 +52,25 @@ class Events:
|
|
|
52
52
|
def list(
|
|
53
53
|
self,
|
|
54
54
|
*,
|
|
55
|
-
app_id: str,
|
|
55
|
+
app_id: str | None = None,
|
|
56
56
|
type: str | None = None,
|
|
57
57
|
created_after: datetime | None = None,
|
|
58
58
|
created_before: datetime | None = None,
|
|
59
59
|
limit: int | None = None,
|
|
60
60
|
opts: CallOptions | None = None,
|
|
61
61
|
) -> Page[Event]:
|
|
62
|
+
"""List events, newest first.
|
|
63
|
+
|
|
64
|
+
``app_id`` is an optional filter. An API-key caller may omit it and get
|
|
65
|
+
their key's own app; passing a different app's ID is refused with
|
|
66
|
+
``permission_denied``. A portal user who omits it gets the events of
|
|
67
|
+
every app in the organization.
|
|
68
|
+
"""
|
|
69
|
+
|
|
62
70
|
def fetch(cursor: str | None) -> PageContents[Event]:
|
|
63
|
-
req = webhook_pb2.ListEventsRequest(
|
|
71
|
+
req = webhook_pb2.ListEventsRequest()
|
|
72
|
+
if app_id is not None:
|
|
73
|
+
req.app_id = app_id
|
|
64
74
|
if type is not None:
|
|
65
75
|
req.type = type
|
|
66
76
|
if created_after is not None:
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Ingest rule resource — watch a bucket, transcode what lands in it.
|
|
2
|
+
|
|
3
|
+
An ingest rule is a standing instruction on one readable storage origin: when
|
|
4
|
+
an object matching its filters appears, create the job its action describes.
|
|
5
|
+
Your storage provider posts object-created events to the rule's
|
|
6
|
+
``endpoint_url``, authenticated with the rule's secret, so no server of yours is
|
|
7
|
+
in the path. Amazon S3 via SNS, Google Cloud Storage via a Pub/Sub push
|
|
8
|
+
subscription, Supabase Storage via a database webhook, and a generic shape for
|
|
9
|
+
anything else are all recognised from the payload.
|
|
10
|
+
|
|
11
|
+
Duplicate deliveries are absorbed: an object is identified by
|
|
12
|
+
(rule, bucket, key, etag) and produces exactly one job.
|
|
13
|
+
|
|
14
|
+
The inbound endpoint itself is not part of this SDK — your storage provider
|
|
15
|
+
calls it, not you.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, cast
|
|
21
|
+
|
|
22
|
+
from .._transport.transport import CallOptions, Transport
|
|
23
|
+
from ..pagination import Page, PageContents
|
|
24
|
+
from ..v1 import ingest_rule_pb2
|
|
25
|
+
from ._helpers import assign_pagination, fill_from_dict, resolve_enum
|
|
26
|
+
|
|
27
|
+
_SERVICE = "transcodely.v1.IngestRuleService"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IngestRules:
|
|
31
|
+
def __init__(self, transport: Transport) -> None:
|
|
32
|
+
self._t = transport
|
|
33
|
+
|
|
34
|
+
def create(self, **kwargs: Any) -> ingest_rule_pb2.CreateIngestRuleResponse:
|
|
35
|
+
"""Create a rule on a readable origin.
|
|
36
|
+
|
|
37
|
+
The whole response is returned because ``secret`` rides on it: the
|
|
38
|
+
inbound secret is shown once, here, and cannot be read back. Store it
|
|
39
|
+
wherever the event sender will read it from.
|
|
40
|
+
"""
|
|
41
|
+
req = fill_from_dict(ingest_rule_pb2.CreateIngestRuleRequest(), kwargs)
|
|
42
|
+
return self._t.unary(_SERVICE, "Create", req, ingest_rule_pb2.CreateIngestRuleResponse())
|
|
43
|
+
|
|
44
|
+
def get(self, id: str, opts: CallOptions | None = None) -> ingest_rule_pb2.IngestRule:
|
|
45
|
+
"""Fetch a rule by ID (``ing_*``).
|
|
46
|
+
|
|
47
|
+
The secret is never returned again — the rule carries only
|
|
48
|
+
``secret_prefix`` and ``secret_hint``.
|
|
49
|
+
"""
|
|
50
|
+
req = ingest_rule_pb2.GetIngestRuleRequest(id=id)
|
|
51
|
+
return self._t.unary(
|
|
52
|
+
_SERVICE, "Get", req, ingest_rule_pb2.GetIngestRuleResponse(), opts
|
|
53
|
+
).rule
|
|
54
|
+
|
|
55
|
+
def list(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
origin_id: str | None = None,
|
|
59
|
+
enabled: bool | None = None,
|
|
60
|
+
app_id: str | None = None,
|
|
61
|
+
limit: int | None = None,
|
|
62
|
+
opts: CallOptions | None = None,
|
|
63
|
+
) -> Page[ingest_rule_pb2.IngestRule]:
|
|
64
|
+
"""Rules in scope, newest first."""
|
|
65
|
+
|
|
66
|
+
def fetch(cursor: str | None) -> PageContents[ingest_rule_pb2.IngestRule]:
|
|
67
|
+
req = ingest_rule_pb2.ListIngestRulesRequest()
|
|
68
|
+
if origin_id is not None:
|
|
69
|
+
req.origin_id = origin_id
|
|
70
|
+
if enabled is not None:
|
|
71
|
+
req.enabled = enabled
|
|
72
|
+
if app_id is not None:
|
|
73
|
+
req.app_id = app_id
|
|
74
|
+
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
75
|
+
res = self._t.unary(
|
|
76
|
+
_SERVICE, "List", req, ingest_rule_pb2.ListIngestRulesResponse(), opts
|
|
77
|
+
)
|
|
78
|
+
return PageContents(
|
|
79
|
+
items=list(res.rules), next_cursor=res.pagination.next_cursor or None
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return Page(fetch)
|
|
83
|
+
|
|
84
|
+
def update(self, **kwargs: Any) -> ingest_rule_pb2.UpdateIngestRuleResponse:
|
|
85
|
+
"""Update name, enabled state, filters or action, optionally rotating the secret.
|
|
86
|
+
|
|
87
|
+
The update MERGES: it applies only what it carries, down to the
|
|
88
|
+
individual filters and the individual parts of the action. Narrowing a
|
|
89
|
+
rule to a new prefix is ``filters={"prefix": "raw/"}`` and nothing else
|
|
90
|
+
— the suffix, content-type and size filters are untouched.
|
|
91
|
+
|
|
92
|
+
Removing something rather than changing it takes the two clear flags.
|
|
93
|
+
``clear_filters=True`` empties the filter set before ``filters`` is
|
|
94
|
+
applied, so on its own it widens the rule to everything in the bucket.
|
|
95
|
+
``clear_action=True`` replaces the action outright, and ``action`` must
|
|
96
|
+
then be complete — at least one output and exactly one destination.
|
|
97
|
+
That is the only way to drop an action's thumbnails or metadata, since
|
|
98
|
+
a repeated or map field sent empty reads as "not sent". For the same
|
|
99
|
+
reason ``managed=False`` does not turn managed storage off; it leaves
|
|
100
|
+
the destination alone, so send ``output_origin_id`` instead.
|
|
101
|
+
|
|
102
|
+
The whole response is returned: a rotation puts the new secret on it
|
|
103
|
+
(once, and the previous secret keeps working for 24 hours), and
|
|
104
|
+
switching a paused rule back on reports how many deliveries it declined
|
|
105
|
+
while off in ``events_skipped_while_disabled``.
|
|
106
|
+
"""
|
|
107
|
+
req = fill_from_dict(ingest_rule_pb2.UpdateIngestRuleRequest(), kwargs)
|
|
108
|
+
return self._t.unary(_SERVICE, "Update", req, ingest_rule_pb2.UpdateIngestRuleResponse())
|
|
109
|
+
|
|
110
|
+
def delete(self, id: str, opts: CallOptions | None = None) -> ingest_rule_pb2.IngestRule:
|
|
111
|
+
"""Delete a rule; its endpoint stops accepting events immediately.
|
|
112
|
+
|
|
113
|
+
The events it already received are kept, and the rule as it stood at
|
|
114
|
+
deletion is returned.
|
|
115
|
+
"""
|
|
116
|
+
req = ingest_rule_pb2.DeleteIngestRuleRequest(id=id)
|
|
117
|
+
return self._t.unary(
|
|
118
|
+
_SERVICE, "Delete", req, ingest_rule_pb2.DeleteIngestRuleResponse(), opts
|
|
119
|
+
).rule
|
|
120
|
+
|
|
121
|
+
def list_events(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
rule_id: str | None = None,
|
|
125
|
+
status: int | str | None = None,
|
|
126
|
+
app_id: str | None = None,
|
|
127
|
+
limit: int | None = None,
|
|
128
|
+
opts: CallOptions | None = None,
|
|
129
|
+
) -> Page[ingest_rule_pb2.StorageEvent]:
|
|
130
|
+
"""Every delivery received and what came of it, newest first.
|
|
131
|
+
|
|
132
|
+
``status`` accepts the simplified wire string (``"skipped"``) as well as
|
|
133
|
+
the generated constant.
|
|
134
|
+
"""
|
|
135
|
+
resolved_status = (
|
|
136
|
+
None
|
|
137
|
+
if status is None
|
|
138
|
+
else resolve_enum(status, ingest_rule_pb2.StorageEventStatus.DESCRIPTOR)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def fetch(cursor: str | None) -> PageContents[ingest_rule_pb2.StorageEvent]:
|
|
142
|
+
req = ingest_rule_pb2.ListIngestEventsRequest()
|
|
143
|
+
if rule_id is not None:
|
|
144
|
+
req.rule_id = rule_id
|
|
145
|
+
if resolved_status is not None:
|
|
146
|
+
# resolve_enum returns a plain int; the field is typed as the enum.
|
|
147
|
+
req.status = cast("ingest_rule_pb2.StorageEventStatus", resolved_status)
|
|
148
|
+
if app_id is not None:
|
|
149
|
+
req.app_id = app_id
|
|
150
|
+
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
151
|
+
res = self._t.unary(
|
|
152
|
+
_SERVICE, "ListEvents", req, ingest_rule_pb2.ListIngestEventsResponse(), opts
|
|
153
|
+
)
|
|
154
|
+
return PageContents(
|
|
155
|
+
items=list(res.events), next_cursor=res.pagination.next_cursor or None
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return Page(fetch)
|
|
159
|
+
|
|
160
|
+
def test(self, **kwargs: Any) -> ingest_rule_pb2.TestIngestRuleResponse:
|
|
161
|
+
"""Dry-run an object key against a rule. Nothing is stored, no job is created.
|
|
162
|
+
|
|
163
|
+
Reports whether the filters match and, when they do, the exact job
|
|
164
|
+
request the rule would submit. Supply ``etag`` when you want the
|
|
165
|
+
preview to name the idempotency key a real delivery would carry — the
|
|
166
|
+
key is derived from it.
|
|
167
|
+
"""
|
|
168
|
+
req = fill_from_dict(ingest_rule_pb2.TestIngestRuleRequest(), kwargs)
|
|
169
|
+
return self._t.unary(_SERVICE, "Test", req, ingest_rule_pb2.TestIngestRuleResponse())
|
|
170
|
+
|
|
171
|
+
def replay_event(
|
|
172
|
+
self, event_id: str, opts: CallOptions | None = None
|
|
173
|
+
) -> ingest_rule_pb2.StorageEvent:
|
|
174
|
+
"""Re-queue a skipped or refused event (``sev_*``).
|
|
175
|
+
|
|
176
|
+
Deduplication is permanent: re-sending the event, or re-uploading the
|
|
177
|
+
same bytes, is absorbed and produces nothing. The event is reset rather
|
|
178
|
+
than duplicated, so it keeps its id and its history.
|
|
179
|
+
"""
|
|
180
|
+
req = ingest_rule_pb2.ReplayIngestEventRequest(event_id=event_id)
|
|
181
|
+
return self._t.unary(
|
|
182
|
+
_SERVICE, "ReplayEvent", req, ingest_rule_pb2.ReplayIngestEventResponse(), opts
|
|
183
|
+
).event
|
|
@@ -126,12 +126,22 @@ class WebhookEndpoints:
|
|
|
126
126
|
def list(
|
|
127
127
|
self,
|
|
128
128
|
*,
|
|
129
|
-
app_id: str,
|
|
129
|
+
app_id: str | None = None,
|
|
130
130
|
limit: int | None = None,
|
|
131
131
|
opts: CallOptions | None = None,
|
|
132
132
|
) -> Page[webhook_pb2.WebhookEndpoint]:
|
|
133
|
+
"""List webhook endpoints, newest first.
|
|
134
|
+
|
|
135
|
+
``app_id`` is an optional filter. An API-key caller may omit it and get
|
|
136
|
+
their key's own app; passing a different app's ID is refused with
|
|
137
|
+
``permission_denied``. A portal user who omits it gets the endpoints of
|
|
138
|
+
every app in the organization.
|
|
139
|
+
"""
|
|
140
|
+
|
|
133
141
|
def fetch(cursor: str | None) -> PageContents[webhook_pb2.WebhookEndpoint]:
|
|
134
|
-
req = webhook_pb2.ListWebhookEndpointsRequest(
|
|
142
|
+
req = webhook_pb2.ListWebhookEndpointsRequest()
|
|
143
|
+
if app_id is not None:
|
|
144
|
+
req.app_id = app_id
|
|
135
145
|
pagination = common_pb2.PaginationRequest()
|
|
136
146
|
if limit is not None:
|
|
137
147
|
pagination.limit = limit
|