transcodely 0.3.1__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)``."""
@@ -69,3 +69,50 @@ class Apps:
69
69
  return self._t.unary(
70
70
  _SERVICE, "UpdateHostingConfig", req, app_pb2.UpdateHostingConfigResponse()
71
71
  )
72
+
73
+ def update_spend_limit(
74
+ self,
75
+ app_id: str,
76
+ monthly_spend_limit_eur: Optional[float] = None,
77
+ opts: Optional[CallOptions] = None,
78
+ ) -> app_pb2.App:
79
+ """Set or clear an app's monthly transcoding spend cap.
80
+
81
+ Pass ``monthly_spend_limit_eur`` (must be > 0) to set the cap, or leave it
82
+ ``None`` to clear the cap and return the app to unlimited. ``set_spend_limit``
83
+ and ``clear_spend_limit`` are the ergonomic shorthands.
84
+ """
85
+ req = app_pb2.UpdateSpendLimitRequest(app_id=app_id)
86
+ if monthly_spend_limit_eur is not None:
87
+ # Setting the optional field marks presence; leaving it unset omits it
88
+ # from the request, which the server reads as "clear the cap".
89
+ req.monthly_spend_limit_eur = monthly_spend_limit_eur
90
+ return self._t.unary(
91
+ _SERVICE, "UpdateSpendLimit", req, app_pb2.UpdateSpendLimitResponse(), opts
92
+ ).app
93
+
94
+ def set_spend_limit(
95
+ self, app_id: str, monthly_spend_limit_eur: float, opts: Optional[CallOptions] = None
96
+ ) -> app_pb2.App:
97
+ """Set the app's monthly transcoding spend cap in EUR (must be > 0).
98
+
99
+ Once recorded spend for the current billing period reaches the cap, new
100
+ jobs are rejected with the ``limit_exceeded`` error code; in-flight jobs
101
+ are never stopped. Use :meth:`clear_spend_limit` to return to unlimited.
102
+ """
103
+ return self.update_spend_limit(app_id, monthly_spend_limit_eur, opts)
104
+
105
+ def clear_spend_limit(self, app_id: str, opts: Optional[CallOptions] = None) -> app_pb2.App:
106
+ """Clear the app's monthly spend cap, returning it to unlimited (the default)."""
107
+ return self.update_spend_limit(app_id, None, opts)
108
+
109
+ def get_spend(
110
+ self, app_id: str, opts: Optional[CallOptions] = None
111
+ ) -> app_pb2.GetSpendResponse:
112
+ """Get the app's current-period transcoding spend against its limit.
113
+
114
+ Returns the billing-period bounds, EUR spent so far, the cap (if set), and
115
+ whether the 80% warning and 100% breach events have fired this period.
116
+ """
117
+ req = app_pb2.GetSpendRequest(app_id=app_id)
118
+ return self._t.unary(_SERVICE, "GetSpend", req, app_pb2.GetSpendResponse(), opts)
@@ -0,0 +1,92 @@
1
+ """Billing resource — an organization's invoices, read-only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .._transport.transport import CallOptions, Transport
8
+ from ..pagination import Page, PageContents
9
+ from ..v1 import billing_pb2
10
+ from ._helpers import assign_pagination
11
+
12
+ _SERVICE = "transcodely.v1.BillingService"
13
+
14
+
15
+ class Billing:
16
+ """An organization's billing statements.
17
+
18
+ Unlike every other resource, billing settles a whole organization rather
19
+ than a single app, so it is **not** available to API-key callers: a key is
20
+ scoped to one app, and there is no app-scoped subset of an invoice worth
21
+ serving. An API key gets a :class:`~transcodely.errors.PermissionError`.
22
+
23
+ Reading invoices needs a dashboard session token for an organization
24
+ **owner**, plus the organization the request is for::
25
+
26
+ client = Transcodely(session_token, organization_id="org_f6g7h8i9j0")
27
+ upcoming = client.billing.retrieve_upcoming()
28
+
29
+ Invoices are generated automatically when a period closes. There is no API
30
+ to create, edit, or delete one — a statement records what happened.
31
+
32
+ Monetary amounts are integer minor units (cents) of the invoice currency:
33
+ ``total_cents == 1250`` in EUR is 12.50 EUR.
34
+ """
35
+
36
+ def __init__(self, transport: Transport) -> None:
37
+ self._t = transport
38
+
39
+ def list_invoices(
40
+ self,
41
+ *,
42
+ limit: Optional[int] = None,
43
+ opts: Optional[CallOptions] = None,
44
+ ) -> Page[billing_pb2.Invoice]:
45
+ """Page through finalized invoices, newest period first.
46
+
47
+ Line items are omitted here — use :meth:`retrieve` for one invoice's
48
+ breakdown. A statement still being generated for a just-ended period is
49
+ never returned; for the period currently accruing use
50
+ :meth:`retrieve_upcoming`.
51
+ """
52
+
53
+ def fetch(cursor: Optional[str]) -> PageContents[billing_pb2.Invoice]:
54
+ req = billing_pb2.ListInvoicesRequest()
55
+ assign_pagination(req.pagination, limit=limit, cursor=cursor)
56
+ res = self._t.unary(
57
+ _SERVICE, "ListInvoices", req, billing_pb2.ListInvoicesResponse(), opts
58
+ )
59
+ return PageContents(
60
+ items=list(res.invoices),
61
+ next_cursor=res.pagination.next_cursor or None,
62
+ )
63
+
64
+ return Page(fetch)
65
+
66
+ def retrieve(self, invoice_id: str, opts: Optional[CallOptions] = None) -> billing_pb2.Invoice:
67
+ """Retrieve one invoice by ID (``inv_*``), including its line items."""
68
+ req = billing_pb2.GetInvoiceRequest(id=invoice_id)
69
+ return self._t.unary(
70
+ _SERVICE,
71
+ "GetInvoice",
72
+ req,
73
+ billing_pb2.GetInvoiceResponse(),
74
+ opts,
75
+ ).invoice
76
+
77
+ def retrieve_upcoming(self, opts: Optional[CallOptions] = None) -> billing_pb2.Invoice:
78
+ """Retrieve the statement for the period currently accruing.
79
+
80
+ Computed live from settled jobs rather than stored, so its ``id`` is
81
+ empty, its status is ``draft``, and its totals move as jobs finish. Jobs
82
+ still running are not included at any price — a job is billed only once
83
+ it settles.
84
+ """
85
+ req = billing_pb2.GetUpcomingInvoiceRequest()
86
+ return self._t.unary(
87
+ _SERVICE,
88
+ "GetUpcomingInvoice",
89
+ req,
90
+ billing_pb2.GetUpcomingInvoiceResponse(),
91
+ opts,
92
+ ).invoice
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Generator, Mapping, Optional, Union
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
@@ -24,12 +24,15 @@ class Jobs:
24
24
  *,
25
25
  output_origin_id: Optional[str] = None,
26
26
  output_path_template: Optional[str] = None,
27
+ managed: Optional[bool] = None,
27
28
  input_origin_id: Optional[str] = None,
28
29
  input_path: Optional[str] = None,
30
+ input_video_id: Optional[str] = None,
29
31
  priority: Optional[Union[int, str]] = None,
30
32
  delayed_start: Optional[bool] = None,
31
33
  webhook_url: Optional[str] = None,
32
34
  metadata: Optional[Mapping[str, str]] = None,
35
+ clip: Optional[Union[job_pb2.ClipConfig, Mapping[str, Any]]] = None,
33
36
  idempotency_key: Optional[str] = None,
34
37
  request: Optional[job_pb2.CreateJobRequest] = None,
35
38
  opts: Optional[CallOptions] = None,
@@ -39,25 +42,46 @@ class Jobs:
39
42
  Keyword arguments mirror the API's snake_case fields. Enum-valued fields —
40
43
  ``priority`` here, plus any enum inside ``outputs`` dicts — accept either the
41
44
  simplified lowercase string (``"standard"``, ``"h264"``) or the raw proto int.
42
- Provide inputs via ``input_url`` (direct URL) OR ``input_origin_id`` +
43
- ``input_path`` (origin mode); the server rejects both. Pass a fully-built
44
- ``request=`` to bypass the convenience kwargs entirely. ``idempotency_key`` is
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
45
50
  auto-filled with a uuid4 when omitted. ``output_path_template`` overrides where
46
51
  rendered outputs are written within the output origin (e.g.
47
- ``"videos/{job_id}/{output}"``).
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``.
48
63
  """
49
64
  if request is None:
50
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)
51
71
  if input_url is not None:
52
72
  payload["input_url"] = input_url
53
73
  if input_origin_id is not None:
54
74
  payload["input_origin_id"] = input_origin_id
55
75
  if input_path is not None:
56
76
  payload["input_path"] = input_path
77
+ if input_video_id is not None:
78
+ payload["input_video_id"] = input_video_id
57
79
  if output_origin_id is not None:
58
80
  payload["output_origin_id"] = output_origin_id
59
81
  if output_path_template is not None:
60
82
  payload["output_path_template"] = output_path_template
83
+ if managed is not None:
84
+ payload["managed"] = managed
61
85
  if outputs:
62
86
  payload["outputs"] = list(outputs)
63
87
  if priority is not None:
@@ -70,6 +94,8 @@ class Jobs:
70
94
  payload["metadata"] = dict(metadata)
71
95
  req = job_pb2.CreateJobRequest()
72
96
  fill_from_dict(req, payload)
97
+ if clip_msg is not None:
98
+ req.clip.CopyFrom(clip_msg)
73
99
  else:
74
100
  req = request
75
101
  if not req.idempotency_key:
@@ -110,7 +136,8 @@ class Jobs:
110
136
  def fetch(cursor: Optional[str]) -> PageContents[job_pb2.Job]:
111
137
  req = job_pb2.ListJobsRequest()
112
138
  if status_value is not None:
113
- req.status = status_value
139
+ # resolve_enum returns a plain int; the field is typed as the enum.
140
+ req.status = cast("job_pb2.JobStatus", status_value)
114
141
  assign_pagination(
115
142
  req.pagination,
116
143
  limit=eff_limit,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Optional, Union
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
@@ -37,7 +37,8 @@ class Memberships:
37
37
  def fetch(cursor: Optional[str]) -> PageContents[membership_pb2.MembershipWithUser]:
38
38
  req = membership_pb2.ListMembershipsRequest()
39
39
  if status_value is not None:
40
- req.status = status_value
40
+ # resolve_enum returns a plain int; the field is typed as the enum.
41
+ req.status = cast("membership_pb2.MembershipStatus", status_value)
41
42
  assign_pagination(req.pagination, limit=limit, cursor=cursor)
42
43
  res = self._t.unary(
43
44
  _SERVICE, "List", req, membership_pb2.ListMembershipsResponse(), opts
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Optional, Union
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
@@ -48,7 +48,8 @@ class Users:
48
48
  def fetch(cursor: Optional[str]) -> PageContents[user_pb2.User]:
49
49
  req = user_pb2.ListUsersRequest()
50
50
  if status_value is not None:
51
- req.status = status_value
51
+ # resolve_enum returns a plain int; the field is typed as the enum.
52
+ req.status = cast("user_pb2.UserStatus", status_value)
52
53
  assign_pagination(req.pagination, limit=limit, cursor=cursor)
53
54
  res = self._t.unary(_SERVICE, "List", req, user_pb2.ListUsersResponse(), opts)
54
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())