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 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 assign_pagination(req_pagination: Any, *, limit: Optional[int], cursor: Optional[str]) -> None:
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
@@ -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
@@ -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
- idempotency_key: Optional[str] = None,
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. Auto-fills ``idempotency_key`` if omitted."""
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
- req = job_pb2.CreateJobRequest()
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
- req.input_url = input_url
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
- from google.protobuf import json_format
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
- req.priority = priority
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
- for k, v in metadata.items():
52
- req.metadata[k] = v
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
- from ..v1 import common_pb2
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 status is not None:
78
- req.status = status
79
- pagination = common_pb2.PaginationRequest()
80
- if limit is not None:
81
- pagination.limit = limit
82
- if cursor is not None:
83
- pagination.cursor = cursor
84
- req.pagination.CopyFrom(pagination)
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
@@ -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(
@@ -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())