transcodely 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl
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/__init__.py +20 -0
- transcodely/_transport/transport.py +4 -0
- transcodely/client.py +16 -0
- transcodely/resources/_helpers.py +40 -3
- transcodely/resources/apps.py +59 -0
- transcodely/resources/billing.py +92 -0
- transcodely/resources/jobs.py +92 -30
- transcodely/resources/memberships.py +17 -2
- transcodely/resources/users.py +15 -2
- transcodely/resources/videos.py +39 -0
- transcodely/types.py +86 -0
- transcodely/v1/app_pb2.py +52 -38
- transcodely/v1/app_pb2.pyi +48 -4
- transcodely/v1/billing_pb2.py +65 -0
- transcodely/v1/billing_pb2.pyi +131 -0
- transcodely/v1/job_pb2.py +86 -65
- transcodely/v1/job_pb2.pyi +70 -8
- transcodely/v1/subtitles_pb2.py +13 -7
- transcodely/v1/subtitles_pb2.pyi +56 -3
- transcodely/v1/thumbnails_pb2.py +14 -8
- transcodely/v1/thumbnails_pb2.pyi +10 -2
- transcodely/v1/video_pb2.py +106 -64
- transcodely/v1/video_pb2.pyi +168 -10
- transcodely/v1/watermark_pb2.py +57 -0
- transcodely/v1/watermark_pb2.pyi +56 -0
- transcodely/v1/webhook_pb2.py +68 -64
- transcodely/v1/webhook_pb2.pyi +22 -6
- transcodely/version.py +1 -1
- transcodely/webhooks/types.py +28 -4
- {transcodely-0.3.0.dist-info → transcodely-0.3.2.dist-info}/METADATA +87 -4
- {transcodely-0.3.0.dist-info → transcodely-0.3.2.dist-info}/RECORD +33 -28
- {transcodely-0.3.0.dist-info → transcodely-0.3.2.dist-info}/WHEEL +0 -0
- {transcodely-0.3.0.dist-info → transcodely-0.3.2.dist-info}/licenses/LICENSE +0 -0
transcodely/__init__.py
CHANGED
|
@@ -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)
|
transcodely/client.py
CHANGED
|
@@ -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)``."""
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, Mapping, Optional
|
|
5
|
+
from typing import Any, Mapping, Optional, Union
|
|
6
6
|
|
|
7
7
|
from google.protobuf import json_format
|
|
8
|
+
from google.protobuf.descriptor import EnumDescriptor
|
|
8
9
|
from google.protobuf.message import Message
|
|
9
10
|
|
|
10
|
-
from .._codec.json_codec import transform_enums_in_dict
|
|
11
|
+
from .._codec.json_codec import expand_enum_value, simplify_enum_value, transform_enums_in_dict
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def fill_from_dict(msg: Message, data: Mapping[str, Any]) -> Message:
|
|
@@ -22,8 +23,44 @@ def fill_from_dict(msg: Message, data: Mapping[str, Any]) -> Message:
|
|
|
22
23
|
return msg
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def
|
|
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:
|
|
26
61
|
if limit is not None:
|
|
27
62
|
req_pagination.limit = limit
|
|
28
63
|
if cursor is not None:
|
|
29
64
|
req_pagination.cursor = cursor
|
|
65
|
+
if offset is not None:
|
|
66
|
+
req_pagination.offset = offset
|
transcodely/resources/apps.py
CHANGED
|
@@ -31,11 +31,23 @@ class Apps:
|
|
|
31
31
|
def list(
|
|
32
32
|
self,
|
|
33
33
|
*,
|
|
34
|
+
org_id: Optional[str] = None,
|
|
34
35
|
limit: Optional[int] = None,
|
|
36
|
+
include_archived: Optional[bool] = None,
|
|
35
37
|
opts: Optional[CallOptions] = None,
|
|
36
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
|
+
|
|
37
45
|
def fetch(cursor: Optional[str]) -> PageContents[app_pb2.App]:
|
|
38
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
|
|
39
51
|
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
40
52
|
res = self._t.unary(_SERVICE, "List", req, app_pb2.ListAppsResponse(), opts)
|
|
41
53
|
return PageContents(
|
|
@@ -57,3 +69,50 @@ class Apps:
|
|
|
57
69
|
return self._t.unary(
|
|
58
70
|
_SERVICE, "UpdateHostingConfig", req, app_pb2.UpdateHostingConfigResponse()
|
|
59
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
|
transcodely/resources/jobs.py
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, Generator, Mapping, Optional
|
|
5
|
+
from typing import Any, Generator, Mapping, Optional, Union, cast
|
|
6
6
|
|
|
7
7
|
from .._transport.transport import CallOptions, Transport
|
|
8
8
|
from ..pagination import Page, PageContents
|
|
9
9
|
from ..streaming import watch
|
|
10
10
|
from ..v1 import job_pb2
|
|
11
|
+
from ._helpers import assign_pagination, fill_from_dict, resolve_enum
|
|
11
12
|
|
|
12
13
|
_SERVICE = "transcodely.v1.JobService"
|
|
13
14
|
|
|
@@ -21,35 +22,80 @@ class Jobs:
|
|
|
21
22
|
input_url: Optional[str] = None,
|
|
22
23
|
outputs: Optional[list[Mapping[str, Any]]] = None,
|
|
23
24
|
*,
|
|
24
|
-
|
|
25
|
+
output_origin_id: Optional[str] = None,
|
|
26
|
+
output_path_template: Optional[str] = None,
|
|
27
|
+
managed: Optional[bool] = None,
|
|
28
|
+
input_origin_id: Optional[str] = None,
|
|
29
|
+
input_path: Optional[str] = None,
|
|
30
|
+
input_video_id: Optional[str] = None,
|
|
31
|
+
priority: Optional[Union[int, str]] = None,
|
|
32
|
+
delayed_start: Optional[bool] = None,
|
|
25
33
|
webhook_url: Optional[str] = None,
|
|
26
|
-
priority: Optional[int] = None,
|
|
27
34
|
metadata: Optional[Mapping[str, str]] = None,
|
|
35
|
+
clip: Optional[Union[job_pb2.ClipConfig, Mapping[str, Any]]] = None,
|
|
36
|
+
idempotency_key: Optional[str] = None,
|
|
28
37
|
request: Optional[job_pb2.CreateJobRequest] = None,
|
|
29
38
|
opts: Optional[CallOptions] = None,
|
|
30
39
|
) -> job_pb2.Job:
|
|
31
|
-
"""Create a new transcoding job.
|
|
40
|
+
"""Create a new transcoding job.
|
|
41
|
+
|
|
42
|
+
Keyword arguments mirror the API's snake_case fields. Enum-valued fields —
|
|
43
|
+
``priority`` here, plus any enum inside ``outputs`` dicts — accept either the
|
|
44
|
+
simplified lowercase string (``"standard"``, ``"h264"``) or the raw proto int.
|
|
45
|
+
Provide inputs via exactly one of: ``input_url`` (direct URL),
|
|
46
|
+
``input_origin_id`` + ``input_path`` (origin mode), or ``input_video_id``
|
|
47
|
+
(a hosted ``vid_`` video, e.g. to retro-caption it); the server rejects
|
|
48
|
+
more than one. Pass a fully-built ``request=`` to bypass the convenience
|
|
49
|
+
kwargs entirely. ``idempotency_key`` is
|
|
50
|
+
auto-filled with a uuid4 when omitted. ``output_path_template`` overrides where
|
|
51
|
+
rendered outputs are written within the output origin (e.g.
|
|
52
|
+
``"videos/{job_id}/{output}"``). Set ``managed=True`` to write outputs to
|
|
53
|
+
Transcodely-managed hosting storage and create a video record; managed apps do
|
|
54
|
+
not require an ``output_origin_id``.
|
|
55
|
+
|
|
56
|
+
``clip`` trims the input to a sub-range before encoding — pass a
|
|
57
|
+
:class:`~transcodely.types.ClipConfig` or a plain dict like
|
|
58
|
+
``{"start_seconds": 2, "end_seconds": 7}``. It applies job-wide (every output
|
|
59
|
+
and thumbnails/sprites are computed within the range) and reduces cost, since
|
|
60
|
+
billing keys off the produced output duration. Omit ``end_seconds`` (or leave it
|
|
61
|
+
``0``) to encode through to the end of the input; a range outside the probed
|
|
62
|
+
input duration fails the job at probe time with ``input_clip_out_of_range``.
|
|
63
|
+
"""
|
|
32
64
|
if request is None:
|
|
33
|
-
|
|
65
|
+
payload: dict[str, Any] = {}
|
|
66
|
+
clip_msg: Optional[job_pb2.ClipConfig] = None
|
|
67
|
+
if isinstance(clip, job_pb2.ClipConfig):
|
|
68
|
+
clip_msg = clip
|
|
69
|
+
elif clip is not None:
|
|
70
|
+
payload["clip"] = dict(clip)
|
|
34
71
|
if input_url is not None:
|
|
35
|
-
|
|
72
|
+
payload["input_url"] = input_url
|
|
73
|
+
if input_origin_id is not None:
|
|
74
|
+
payload["input_origin_id"] = input_origin_id
|
|
75
|
+
if input_path is not None:
|
|
76
|
+
payload["input_path"] = input_path
|
|
77
|
+
if input_video_id is not None:
|
|
78
|
+
payload["input_video_id"] = input_video_id
|
|
79
|
+
if output_origin_id is not None:
|
|
80
|
+
payload["output_origin_id"] = output_origin_id
|
|
81
|
+
if output_path_template is not None:
|
|
82
|
+
payload["output_path_template"] = output_path_template
|
|
83
|
+
if managed is not None:
|
|
84
|
+
payload["managed"] = managed
|
|
36
85
|
if outputs:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
from .._codec.json_codec import transform_enums_in_dict
|
|
40
|
-
|
|
41
|
-
# Outputs are nested messages — round-trip through JSON so callers can
|
|
42
|
-
# pass plain dicts with simplified-enum strings.
|
|
43
|
-
payload = {"outputs": list(outputs)}
|
|
44
|
-
transform_enums_in_dict(payload, req.DESCRIPTOR, mode="expand")
|
|
45
|
-
json_format.ParseDict(payload, req, ignore_unknown_fields=True)
|
|
46
|
-
if webhook_url is not None:
|
|
47
|
-
req.webhook_url = webhook_url
|
|
86
|
+
payload["outputs"] = list(outputs)
|
|
48
87
|
if priority is not None:
|
|
49
|
-
|
|
88
|
+
payload["priority"] = priority
|
|
89
|
+
if delayed_start is not None:
|
|
90
|
+
payload["delayed_start"] = delayed_start
|
|
91
|
+
if webhook_url is not None:
|
|
92
|
+
payload["webhook_url"] = webhook_url
|
|
50
93
|
if metadata:
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
payload["metadata"] = dict(metadata)
|
|
95
|
+
req = job_pb2.CreateJobRequest()
|
|
96
|
+
fill_from_dict(req, payload)
|
|
97
|
+
if clip_msg is not None:
|
|
98
|
+
req.clip.CopyFrom(clip_msg)
|
|
53
99
|
else:
|
|
54
100
|
req = request
|
|
55
101
|
if not req.idempotency_key:
|
|
@@ -66,22 +112,38 @@ class Jobs:
|
|
|
66
112
|
def list(
|
|
67
113
|
self,
|
|
68
114
|
*,
|
|
69
|
-
status: Optional[int] = None,
|
|
115
|
+
status: Optional[Union[int, str]] = None,
|
|
70
116
|
limit: Optional[int] = None,
|
|
117
|
+
offset: Optional[int] = None,
|
|
118
|
+
pagination: Optional[Mapping[str, Any]] = None,
|
|
71
119
|
opts: Optional[CallOptions] = None,
|
|
72
120
|
) -> Page[job_pb2.Job]:
|
|
73
|
-
|
|
121
|
+
"""List jobs, optionally filtered by ``status``.
|
|
122
|
+
|
|
123
|
+
``status`` accepts the simplified lowercase string (``"processing"``) or the
|
|
124
|
+
raw ``JobStatus`` int. Pagination may be given as ``limit`` / ``offset``
|
|
125
|
+
keyword args or as a ``pagination={"limit": ..., "offset": ..., "cursor": ...}``
|
|
126
|
+
mapping; explicit keyword args take precedence over the mapping.
|
|
127
|
+
"""
|
|
128
|
+
status_value = (
|
|
129
|
+
resolve_enum(status, job_pb2.JobStatus.DESCRIPTOR) if status is not None else None
|
|
130
|
+
)
|
|
131
|
+
pag = dict(pagination) if pagination else {}
|
|
132
|
+
eff_limit = limit if limit is not None else pag.get("limit")
|
|
133
|
+
eff_offset = offset if offset is not None else pag.get("offset")
|
|
134
|
+
start_cursor = pag.get("cursor")
|
|
74
135
|
|
|
75
136
|
def fetch(cursor: Optional[str]) -> PageContents[job_pb2.Job]:
|
|
76
137
|
req = job_pb2.ListJobsRequest()
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
pagination
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
138
|
+
if status_value is not None:
|
|
139
|
+
# resolve_enum returns a plain int; the field is typed as the enum.
|
|
140
|
+
req.status = cast("job_pb2.JobStatus", status_value)
|
|
141
|
+
assign_pagination(
|
|
142
|
+
req.pagination,
|
|
143
|
+
limit=eff_limit,
|
|
144
|
+
cursor=cursor if cursor is not None else start_cursor,
|
|
145
|
+
offset=eff_offset,
|
|
146
|
+
)
|
|
85
147
|
res = self._t.unary(_SERVICE, "List", req, job_pb2.ListJobsResponse(), opts)
|
|
86
148
|
return PageContents(
|
|
87
149
|
items=list(res.jobs),
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, Optional
|
|
5
|
+
from typing import Any, Optional, Union, cast
|
|
6
6
|
|
|
7
7
|
from .._transport.transport import CallOptions, Transport
|
|
8
8
|
from ..pagination import Page, PageContents
|
|
9
9
|
from ..v1 import membership_pb2
|
|
10
|
-
from ._helpers import assign_pagination, fill_from_dict
|
|
10
|
+
from ._helpers import assign_pagination, fill_from_dict, resolve_enum
|
|
11
11
|
|
|
12
12
|
_SERVICE = "transcodely.v1.MembershipService"
|
|
13
13
|
|
|
@@ -19,11 +19,26 @@ class Memberships:
|
|
|
19
19
|
def list(
|
|
20
20
|
self,
|
|
21
21
|
*,
|
|
22
|
+
status: Optional[Union[int, str]] = None,
|
|
22
23
|
limit: Optional[int] = None,
|
|
23
24
|
opts: Optional[CallOptions] = None,
|
|
24
25
|
) -> Page[membership_pb2.MembershipWithUser]:
|
|
26
|
+
"""List memberships, optionally filtered by ``status``.
|
|
27
|
+
|
|
28
|
+
``status`` accepts the simplified lowercase string (``"active"``,
|
|
29
|
+
``"invited"``, ``"suspended"``) or the raw ``MembershipStatus`` int.
|
|
30
|
+
"""
|
|
31
|
+
status_value = (
|
|
32
|
+
resolve_enum(status, membership_pb2.MembershipStatus.DESCRIPTOR)
|
|
33
|
+
if status is not None
|
|
34
|
+
else None
|
|
35
|
+
)
|
|
36
|
+
|
|
25
37
|
def fetch(cursor: Optional[str]) -> PageContents[membership_pb2.MembershipWithUser]:
|
|
26
38
|
req = membership_pb2.ListMembershipsRequest()
|
|
39
|
+
if status_value is not None:
|
|
40
|
+
# resolve_enum returns a plain int; the field is typed as the enum.
|
|
41
|
+
req.status = cast("membership_pb2.MembershipStatus", status_value)
|
|
27
42
|
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
28
43
|
res = self._t.unary(
|
|
29
44
|
_SERVICE, "List", req, membership_pb2.ListMembershipsResponse(), opts
|
transcodely/resources/users.py
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any, Optional
|
|
5
|
+
from typing import Any, Optional, Union, cast
|
|
6
6
|
|
|
7
7
|
from .._transport.transport import CallOptions, Transport
|
|
8
8
|
from ..pagination import Page, PageContents
|
|
9
9
|
from ..v1 import user_pb2
|
|
10
|
-
from ._helpers import assign_pagination, fill_from_dict
|
|
10
|
+
from ._helpers import assign_pagination, fill_from_dict, resolve_enum
|
|
11
11
|
|
|
12
12
|
_SERVICE = "transcodely.v1.UserService"
|
|
13
13
|
|
|
@@ -32,11 +32,24 @@ class Users:
|
|
|
32
32
|
def list(
|
|
33
33
|
self,
|
|
34
34
|
*,
|
|
35
|
+
status: Optional[Union[int, str]] = None,
|
|
35
36
|
limit: Optional[int] = None,
|
|
36
37
|
opts: Optional[CallOptions] = None,
|
|
37
38
|
) -> Page[user_pb2.User]:
|
|
39
|
+
"""List users, optionally filtered by ``status``.
|
|
40
|
+
|
|
41
|
+
``status`` accepts the simplified lowercase string (``"active"``,
|
|
42
|
+
``"suspended"``, ``"deleted"``) or the raw ``UserStatus`` int.
|
|
43
|
+
"""
|
|
44
|
+
status_value = (
|
|
45
|
+
resolve_enum(status, user_pb2.UserStatus.DESCRIPTOR) if status is not None else None
|
|
46
|
+
)
|
|
47
|
+
|
|
38
48
|
def fetch(cursor: Optional[str]) -> PageContents[user_pb2.User]:
|
|
39
49
|
req = user_pb2.ListUsersRequest()
|
|
50
|
+
if status_value is not None:
|
|
51
|
+
# resolve_enum returns a plain int; the field is typed as the enum.
|
|
52
|
+
req.status = cast("user_pb2.UserStatus", status_value)
|
|
40
53
|
assign_pagination(req.pagination, limit=limit, cursor=cursor)
|
|
41
54
|
res = self._t.unary(_SERVICE, "List", req, user_pb2.ListUsersResponse(), opts)
|
|
42
55
|
return PageContents(
|
transcodely/resources/videos.py
CHANGED
|
@@ -25,6 +25,20 @@ class Videos:
|
|
|
25
25
|
req = fill_from_dict(video_pb2.CompleteUploadRequest(), kwargs)
|
|
26
26
|
return self._t.unary(_SERVICE, "CompleteUpload", req, video_pb2.CompleteUploadResponse())
|
|
27
27
|
|
|
28
|
+
def create_from_url(self, **kwargs: Any) -> video_pb2.Video:
|
|
29
|
+
"""One-call video ingest from a publicly-reachable http(s) URL.
|
|
30
|
+
|
|
31
|
+
No upload step: the API records the video in "processing" and hands
|
|
32
|
+
the URL to the transcoding pipeline. Returns the ``Video`` directly
|
|
33
|
+
(``CreateFromUrlResponse`` carries nothing else) — playback/embed
|
|
34
|
+
URLs populate once it reaches "ready" (watch it or subscribe to
|
|
35
|
+
``video.ready``).
|
|
36
|
+
"""
|
|
37
|
+
req = fill_from_dict(video_pb2.CreateFromUrlRequest(), kwargs)
|
|
38
|
+
return self._t.unary(
|
|
39
|
+
_SERVICE, "CreateFromUrl", req, video_pb2.CreateFromUrlResponse()
|
|
40
|
+
).video
|
|
41
|
+
|
|
28
42
|
def create_multipart_upload(self, **kwargs: Any) -> video_pb2.CreateMultipartUploadResponse:
|
|
29
43
|
req = fill_from_dict(video_pb2.CreateMultipartUploadRequest(), kwargs)
|
|
30
44
|
return self._t.unary(
|
|
@@ -112,3 +126,28 @@ class Videos:
|
|
|
112
126
|
def get_usage(self, **kwargs: Any) -> video_pb2.GetUsageResponse:
|
|
113
127
|
req = fill_from_dict(video_pb2.GetUsageRequest(), kwargs)
|
|
114
128
|
return self._t.unary(_SERVICE, "GetUsage", req, video_pb2.GetUsageResponse())
|
|
129
|
+
|
|
130
|
+
def get_stats(self, **kwargs: Any) -> video_pb2.GetStatsResponse:
|
|
131
|
+
"""Playback analytics for a single video: plays, watch time, and unique
|
|
132
|
+
viewers per UTC day, plus range totals.
|
|
133
|
+
|
|
134
|
+
Pass ``video_id`` (required) and optional ``start_date`` / ``end_date``
|
|
135
|
+
(inclusive, ``YYYY-MM-DD``). Returns the full ``GetStatsResponse`` —
|
|
136
|
+
``.daily`` holds per-day rows (oldest first, activity-only, no zero-fill)
|
|
137
|
+
and ``.totals`` the range aggregate. Stats are best-effort and roll up
|
|
138
|
+
hourly, so recent activity can lag by up to an hour.
|
|
139
|
+
"""
|
|
140
|
+
req = fill_from_dict(video_pb2.GetStatsRequest(), kwargs)
|
|
141
|
+
return self._t.unary(_SERVICE, "GetStats", req, video_pb2.GetStatsResponse())
|
|
142
|
+
|
|
143
|
+
def list_top_videos(self, **kwargs: Any) -> video_pb2.ListTopVideosResponse:
|
|
144
|
+
"""An app's top videos ranked by plays over a date range.
|
|
145
|
+
|
|
146
|
+
Optional kwargs: ``app_id`` (API-key callers may omit it — their key's
|
|
147
|
+
app is used), ``start_date`` / ``end_date`` (inclusive, ``YYYY-MM-DD``),
|
|
148
|
+
and ``limit`` (1-100, default 10). Returns the full
|
|
149
|
+
``ListTopVideosResponse`` — ``.items`` is the ranked list of
|
|
150
|
+
``TopVideo`` rows (by plays, then watch time, descending).
|
|
151
|
+
"""
|
|
152
|
+
req = fill_from_dict(video_pb2.ListTopVideosRequest(), kwargs)
|
|
153
|
+
return self._t.unary(_SERVICE, "ListTopVideos", req, video_pb2.ListTopVideosResponse())
|