transcodely 0.2.0__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.
Files changed (86) hide show
  1. buf/__init__.py +0 -0
  2. buf/validate/__init__.py +0 -0
  3. buf/validate/validate_pb2.py +469 -0
  4. buf/validate/validate_pb2.pyi +651 -0
  5. transcodely/__init__.py +175 -0
  6. transcodely/_codec/__init__.py +0 -0
  7. transcodely/_codec/json_codec.py +193 -0
  8. transcodely/_transport/__init__.py +0 -0
  9. transcodely/_transport/error_mapping.py +102 -0
  10. transcodely/_transport/headers.py +31 -0
  11. transcodely/_transport/transport.py +312 -0
  12. transcodely/client.py +160 -0
  13. transcodely/errors.py +115 -0
  14. transcodely/pagination.py +53 -0
  15. transcodely/py.typed +0 -0
  16. transcodely/resources/__init__.py +0 -0
  17. transcodely/resources/_helpers.py +29 -0
  18. transcodely/resources/api_keys.py +48 -0
  19. transcodely/resources/apps.py +59 -0
  20. transcodely/resources/events.py +118 -0
  21. transcodely/resources/health.py +19 -0
  22. transcodely/resources/jobs.py +120 -0
  23. transcodely/resources/memberships.py +54 -0
  24. transcodely/resources/organizations.py +73 -0
  25. transcodely/resources/origins.py +55 -0
  26. transcodely/resources/presets.py +63 -0
  27. transcodely/resources/users.py +46 -0
  28. transcodely/resources/videos.py +114 -0
  29. transcodely/resources/webhook_endpoints.py +242 -0
  30. transcodely/streaming.py +47 -0
  31. transcodely/types.py +570 -0
  32. transcodely/v1/api_key_pb2.py +69 -0
  33. transcodely/v1/api_key_pb2.pyi +97 -0
  34. transcodely/v1/app_pb2.py +107 -0
  35. transcodely/v1/app_pb2.pyi +219 -0
  36. transcodely/v1/codec_av1_pb2.py +55 -0
  37. transcodely/v1/codec_av1_pb2.pyi +34 -0
  38. transcodely/v1/codec_h264_pb2.py +71 -0
  39. transcodely/v1/codec_h264_pb2.pyi +44 -0
  40. transcodely/v1/codec_h265_pb2.py +69 -0
  41. transcodely/v1/codec_h265_pb2.pyi +42 -0
  42. transcodely/v1/codec_vp9_pb2.py +57 -0
  43. transcodely/v1/codec_vp9_pb2.pyi +30 -0
  44. transcodely/v1/common_pb2.py +67 -0
  45. transcodely/v1/common_pb2.pyi +171 -0
  46. transcodely/v1/content_aware_pb2.py +56 -0
  47. transcodely/v1/content_aware_pb2.pyi +51 -0
  48. transcodely/v1/drm_pb2.py +65 -0
  49. transcodely/v1/drm_pb2.pyi +68 -0
  50. transcodely/v1/hdr_pb2.py +57 -0
  51. transcodely/v1/hdr_pb2.pyi +64 -0
  52. transcodely/v1/health_pb2.py +44 -0
  53. transcodely/v1/health_pb2.pyi +44 -0
  54. transcodely/v1/job_pb2.py +201 -0
  55. transcodely/v1/job_pb2.pyi +546 -0
  56. transcodely/v1/media_pb2.py +43 -0
  57. transcodely/v1/media_pb2.pyi +100 -0
  58. transcodely/v1/membership_pb2.py +71 -0
  59. transcodely/v1/membership_pb2.pyi +122 -0
  60. transcodely/v1/organization_pb2.py +81 -0
  61. transcodely/v1/organization_pb2.pyi +121 -0
  62. transcodely/v1/origin_pb2.py +157 -0
  63. transcodely/v1/origin_pb2.pyi +317 -0
  64. transcodely/v1/preset_pb2.py +192 -0
  65. transcodely/v1/preset_pb2.pyi +290 -0
  66. transcodely/v1/streaming_pb2.py +59 -0
  67. transcodely/v1/streaming_pb2.pyi +56 -0
  68. transcodely/v1/subtitles_pb2.py +69 -0
  69. transcodely/v1/subtitles_pb2.pyi +75 -0
  70. transcodely/v1/thumbnails_pb2.py +63 -0
  71. transcodely/v1/thumbnails_pb2.pyi +74 -0
  72. transcodely/v1/user_pb2.py +72 -0
  73. transcodely/v1/user_pb2.pyi +137 -0
  74. transcodely/v1/video_pb2.py +166 -0
  75. transcodely/v1/video_pb2.pyi +367 -0
  76. transcodely/v1/webhook_pb2.py +151 -0
  77. transcodely/v1/webhook_pb2.pyi +336 -0
  78. transcodely/version.py +11 -0
  79. transcodely/webhooks/__init__.py +44 -0
  80. transcodely/webhooks/construct_event.py +111 -0
  81. transcodely/webhooks/signature.py +110 -0
  82. transcodely/webhooks/types.py +115 -0
  83. transcodely-0.2.0.dist-info/METADATA +349 -0
  84. transcodely-0.2.0.dist-info/RECORD +86 -0
  85. transcodely-0.2.0.dist-info/WHEEL +4 -0
  86. transcodely-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,175 @@
1
+ """Official Python SDK for the Transcodely video transcoding API.
2
+
3
+ Quick start:
4
+
5
+ .. code-block:: python
6
+
7
+ import os
8
+ from transcodely import Transcodely
9
+
10
+ with Transcodely(api_key=os.environ["TRANSCODELY_API_KEY"]) as client:
11
+ job = client.jobs.create(
12
+ input_url="https://example.com/source.mp4",
13
+ outputs=[{
14
+ "type": "hls",
15
+ "video": [
16
+ {"codec": "h264", "resolution": "1080p"},
17
+ {"codec": "h264", "resolution": "720p"},
18
+ ],
19
+ }],
20
+ )
21
+ print(job.id)
22
+
23
+ for event in client.jobs.watch(job.id):
24
+ print(event.job.status, event.job.progress)
25
+
26
+ Headline message and enum classes are re-exported here for ergonomics. Every
27
+ public proto class is reachable via :mod:`transcodely.types`. Generated
28
+ modules also remain available at ``transcodely.v1.{name}_pb2`` for power users.
29
+ """
30
+
31
+ from .client import Transcodely
32
+ from .errors import (
33
+ APIConnectionError,
34
+ APIError,
35
+ AuthenticationError,
36
+ ConflictError,
37
+ FieldViolation,
38
+ InvalidRequestError,
39
+ NotFoundError,
40
+ PermissionError,
41
+ PreconditionError,
42
+ RateLimitError,
43
+ TranscodelyError,
44
+ WebhookError,
45
+ WebhookPayloadError,
46
+ WebhookSignatureError,
47
+ WebhookTimestampError,
48
+ )
49
+ from .pagination import Page, PageContents
50
+ from .version import API_VERSION, DEFAULT_BASE_URL, SDK_VERSION
51
+ from .webhooks import (
52
+ DEFAULT_TOLERANCE_SECONDS,
53
+ EVENT_ID_HEADER,
54
+ SIGNATURE_HEADER,
55
+ Event,
56
+ EventRequest,
57
+ EventType,
58
+ WebhookEvent,
59
+ Webhooks,
60
+ construct_event,
61
+ verify_signature,
62
+ )
63
+
64
+ # Headline message classes — most callers reach for these. Everything else lives
65
+ # in :mod:`transcodely.types`.
66
+ from .types import (
67
+ APIKey,
68
+ App,
69
+ AudioTrackConfig,
70
+ AutoABRConfig,
71
+ ContentAwareConfig,
72
+ DASHConfig,
73
+ DRMConfig,
74
+ HDRConfig,
75
+ HLSConfig,
76
+ Job,
77
+ JobOutput,
78
+ JobPriority,
79
+ JobStatus,
80
+ Membership,
81
+ Organization,
82
+ Origin,
83
+ OutputFormat,
84
+ OutputSpec,
85
+ OutputStatus,
86
+ Preset,
87
+ PresetVariant,
88
+ Resolution,
89
+ StreamingConfig,
90
+ SubtitleTrack,
91
+ ThumbnailResult,
92
+ ThumbnailSpec,
93
+ User,
94
+ UserWithOrganizations,
95
+ Video,
96
+ VideoCodec,
97
+ VideoRendition,
98
+ VideoVariant,
99
+ WatchEventType,
100
+ WatchJobResponse,
101
+ WatchVideoResponse,
102
+ WebhookDelivery,
103
+ WebhookEndpoint,
104
+ )
105
+
106
+ __all__ = [
107
+ "API_VERSION",
108
+ "APIConnectionError",
109
+ "APIError",
110
+ "APIKey",
111
+ "App",
112
+ "AudioTrackConfig",
113
+ "AuthenticationError",
114
+ "AutoABRConfig",
115
+ "ConflictError",
116
+ "construct_event",
117
+ "ContentAwareConfig",
118
+ "DASHConfig",
119
+ "DEFAULT_BASE_URL",
120
+ "DEFAULT_TOLERANCE_SECONDS",
121
+ "DRMConfig",
122
+ "Event",
123
+ "EVENT_ID_HEADER",
124
+ "EventRequest",
125
+ "EventType",
126
+ "FieldViolation",
127
+ "HDRConfig",
128
+ "HLSConfig",
129
+ "InvalidRequestError",
130
+ "Job",
131
+ "JobOutput",
132
+ "JobPriority",
133
+ "JobStatus",
134
+ "Membership",
135
+ "NotFoundError",
136
+ "Organization",
137
+ "Origin",
138
+ "OutputFormat",
139
+ "OutputSpec",
140
+ "OutputStatus",
141
+ "Page",
142
+ "PageContents",
143
+ "PermissionError",
144
+ "PreconditionError",
145
+ "Preset",
146
+ "PresetVariant",
147
+ "RateLimitError",
148
+ "Resolution",
149
+ "SDK_VERSION",
150
+ "SIGNATURE_HEADER",
151
+ "StreamingConfig",
152
+ "SubtitleTrack",
153
+ "ThumbnailResult",
154
+ "ThumbnailSpec",
155
+ "Transcodely",
156
+ "TranscodelyError",
157
+ "User",
158
+ "UserWithOrganizations",
159
+ "verify_signature",
160
+ "Video",
161
+ "VideoCodec",
162
+ "VideoRendition",
163
+ "VideoVariant",
164
+ "WatchEventType",
165
+ "WatchJobResponse",
166
+ "WatchVideoResponse",
167
+ "WebhookDelivery",
168
+ "WebhookEndpoint",
169
+ "WebhookError",
170
+ "WebhookEvent",
171
+ "WebhookPayloadError",
172
+ "Webhooks",
173
+ "WebhookSignatureError",
174
+ "WebhookTimestampError",
175
+ ]
File without changes
@@ -0,0 +1,193 @@
1
+ """JSON serialization for the Transcodely wire format.
2
+
3
+ Wraps protobuf's ``MessageToDict`` / ``ParseDict`` with snake_case field names
4
+ and lowercase simplified enum values, mirroring ``internal/connect/codec.go``
5
+ on the server. Behavior must stay bit-identical with the TypeScript and Go
6
+ codecs — see the cross-language conformance suite.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from typing import Any, TypeVar
14
+
15
+ from google.protobuf import json_format
16
+ from google.protobuf.descriptor import EnumDescriptor, FieldDescriptor
17
+ from google.protobuf.message import Message
18
+
19
+ T = TypeVar("T", bound=Message)
20
+
21
+ _CAMEL_BOUNDARY = re.compile(r"(?<!^)(?=[A-Z])")
22
+
23
+
24
+ def camel_to_screaming_snake(name: str) -> str:
25
+ """``"JobStatus" → "JOB_STATUS"``; ``"HLSSegmentFormat" → "HLS_SEGMENT_FORMAT"``."""
26
+ out: list[str] = []
27
+ for i, ch in enumerate(name):
28
+ if i > 0 and ch.isupper():
29
+ prev = name[i - 1]
30
+ nxt = name[i + 1] if i + 1 < len(name) else ""
31
+ if prev.islower() or (nxt and nxt.islower()):
32
+ out.append("_")
33
+ out.append(ch.upper())
34
+ return "".join(out)
35
+
36
+
37
+ def enum_prefix(enum_desc: EnumDescriptor) -> str:
38
+ """``"JobStatus" → "JOB_STATUS_"`` — the prefix the server strips/restores."""
39
+ return camel_to_screaming_snake(enum_desc.name) + "_"
40
+
41
+
42
+ def simplify_enum_value(value: str, enum_desc: EnumDescriptor) -> str:
43
+ """``"JOB_STATUS_PENDING" → "pending"``."""
44
+ prefix = enum_prefix(enum_desc)
45
+ if value.startswith(prefix):
46
+ return value[len(prefix) :].lower()
47
+ return value.lower()
48
+
49
+
50
+ def expand_enum_value(value: str, enum_desc: EnumDescriptor) -> str:
51
+ """``"pending" → "JOB_STATUS_PENDING"`` (using the enum's full-name set)."""
52
+ if enum_desc.values_by_name.get(value) is not None:
53
+ return value
54
+ candidate = enum_prefix(enum_desc) + value.upper()
55
+ if enum_desc.values_by_name.get(candidate) is not None:
56
+ return candidate
57
+ return value
58
+
59
+
60
+ def transform_enums_in_dict(
61
+ obj: Any,
62
+ msg_desc: Any,
63
+ *,
64
+ mode: str,
65
+ ) -> None:
66
+ """Recursively walk a JSON dict/list tree and translate every enum string.
67
+
68
+ ``mode`` is ``"simplify"`` (outbound) or ``"expand"`` (inbound).
69
+ """
70
+ if not isinstance(obj, dict):
71
+ return
72
+
73
+ for field in msg_desc.fields:
74
+ key = _pick_key(obj, field)
75
+ if key is None:
76
+ continue
77
+ v = obj[key]
78
+ if v is None:
79
+ continue
80
+ _transform_field(obj, key, v, field, mode)
81
+
82
+
83
+ def _pick_key(obj: dict[str, Any], field: FieldDescriptor) -> str | None:
84
+ # camelCase JSON name (default protobuf JSON) takes precedence; proto name
85
+ # accepted as fallback for tolerant parsing.
86
+ if field.json_name in obj:
87
+ return field.json_name
88
+ if field.name in obj:
89
+ return field.name
90
+ return None
91
+
92
+
93
+ def _transform_field(
94
+ parent: dict[str, Any],
95
+ key: str,
96
+ v: Any,
97
+ field: FieldDescriptor,
98
+ mode: str,
99
+ ) -> None:
100
+ # Map fields: protobuf models them as repeated message of generated entry types.
101
+ if field.message_type and field.message_type.GetOptions().map_entry:
102
+ if not isinstance(v, dict):
103
+ return
104
+ value_field = field.message_type.fields_by_name["value"]
105
+ if value_field.type == FieldDescriptor.TYPE_ENUM:
106
+ enum_desc = value_field.enum_type
107
+ for k, item in list(v.items()):
108
+ if isinstance(item, str):
109
+ v[k] = _apply_enum(item, enum_desc, mode)
110
+ elif value_field.type == FieldDescriptor.TYPE_MESSAGE:
111
+ for k, item in list(v.items()):
112
+ if isinstance(item, dict):
113
+ transform_enums_in_dict(item, value_field.message_type, mode=mode)
114
+ return
115
+
116
+ if field.is_repeated:
117
+ if not isinstance(v, list):
118
+ return
119
+ if field.type == FieldDescriptor.TYPE_ENUM:
120
+ enum_desc = field.enum_type
121
+ for i, item in enumerate(v):
122
+ if isinstance(item, str):
123
+ v[i] = _apply_enum(item, enum_desc, mode)
124
+ elif field.type == FieldDescriptor.TYPE_MESSAGE:
125
+ for item in v:
126
+ if isinstance(item, dict):
127
+ transform_enums_in_dict(item, field.message_type, mode=mode)
128
+ return
129
+
130
+ if field.type == FieldDescriptor.TYPE_ENUM:
131
+ if isinstance(v, str):
132
+ parent[key] = _apply_enum(v, field.enum_type, mode)
133
+ return
134
+
135
+ if field.type == FieldDescriptor.TYPE_MESSAGE:
136
+ if isinstance(v, dict):
137
+ transform_enums_in_dict(v, field.message_type, mode=mode)
138
+
139
+
140
+ def _apply_enum(value: str, enum_desc: EnumDescriptor, mode: str) -> str:
141
+ return (
142
+ simplify_enum_value(value, enum_desc)
143
+ if mode == "simplify"
144
+ else expand_enum_value(value, enum_desc)
145
+ )
146
+
147
+
148
+ def _message_to_dict(msg: Message) -> dict[str, Any]:
149
+ """``MessageToDict`` with the right kwarg for both protobuf v5 and v6."""
150
+ try:
151
+ return json_format.MessageToDict( # type: ignore[call-arg]
152
+ msg,
153
+ preserving_proto_field_name=True,
154
+ always_print_fields_with_no_presence=True,
155
+ use_integers_for_enums=False,
156
+ )
157
+ except TypeError:
158
+ # protobuf < 5.27 used `including_default_value_fields`
159
+ return json_format.MessageToDict( # type: ignore[call-arg]
160
+ msg,
161
+ preserving_proto_field_name=True,
162
+ including_default_value_fields=True,
163
+ use_integers_for_enums=False,
164
+ )
165
+
166
+
167
+ def serialize(msg: Message) -> bytes:
168
+ """Encode a proto message as Transcodely-wire JSON bytes."""
169
+ obj = _message_to_dict(msg)
170
+ transform_enums_in_dict(obj, msg.DESCRIPTOR, mode="simplify")
171
+ return json.dumps(obj).encode("utf-8")
172
+
173
+
174
+ def deserialize(data: bytes, msg: T) -> T:
175
+ """Decode Transcodely-wire JSON bytes into ``msg`` (mutated in place, returned for chaining)."""
176
+ if not data:
177
+ return msg
178
+ parsed = json.loads(data.decode("utf-8"))
179
+ if isinstance(parsed, dict):
180
+ transform_enums_in_dict(parsed, msg.DESCRIPTOR, mode="expand")
181
+ json_format.ParseDict(parsed, msg, ignore_unknown_fields=True)
182
+ return msg
183
+
184
+
185
+ __all__ = [
186
+ "camel_to_screaming_snake",
187
+ "deserialize",
188
+ "enum_prefix",
189
+ "expand_enum_value",
190
+ "serialize",
191
+ "simplify_enum_value",
192
+ "transform_enums_in_dict",
193
+ ]
File without changes
@@ -0,0 +1,102 @@
1
+ """Map a Connect-RPC error response (HTTP status + JSON body) to a typed exception."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Mapping, Optional
7
+
8
+ from ..errors import (
9
+ APIConnectionError,
10
+ APIError,
11
+ AuthenticationError,
12
+ ConflictError,
13
+ FieldViolation,
14
+ InvalidRequestError,
15
+ NotFoundError,
16
+ PermissionError,
17
+ PreconditionError,
18
+ RateLimitError,
19
+ TranscodelyError,
20
+ )
21
+
22
+
23
+ def map_http_error(*, status: int, body: Any, headers: Mapping[str, str]) -> TranscodelyError:
24
+ request_id = _header(headers, "x-request-id")
25
+ parsed = _parse_body(body)
26
+ message = parsed.get("message") or f"HTTP {status}"
27
+ detail = (parsed.get("details") or [{}])[0].get("debug") or {}
28
+ code = detail.get("code")
29
+ raw_violations = detail.get("field_violations") or detail.get("fieldViolations") or []
30
+ violations = [
31
+ FieldViolation(field=v.get("field", ""), description=v.get("description", ""))
32
+ for v in raw_violations
33
+ if isinstance(v, dict)
34
+ ]
35
+ kwargs = {
36
+ "code": code,
37
+ "type": parsed.get("code"),
38
+ "errors": violations,
39
+ "http_status": status,
40
+ "request_id": request_id,
41
+ "raw": body,
42
+ }
43
+
44
+ if status == 401:
45
+ return AuthenticationError(message, **kwargs)
46
+ if status == 403:
47
+ return PermissionError(message, **kwargs)
48
+ if status == 404:
49
+ return NotFoundError(message, **kwargs)
50
+ if status == 409:
51
+ return ConflictError(message, **kwargs)
52
+ if status == 412:
53
+ return PreconditionError(message, **kwargs)
54
+ if status == 422:
55
+ return InvalidRequestError(message, **kwargs)
56
+ if status == 429:
57
+ retry_after = _header(headers, "retry-after")
58
+ retry_after_ms: Optional[int] = None
59
+ if retry_after:
60
+ try:
61
+ retry_after_ms = max(0, int(retry_after) * 1000)
62
+ except ValueError:
63
+ retry_after_ms = None
64
+ return RateLimitError(message, retry_after_ms=retry_after_ms, **kwargs)
65
+ if status >= 500:
66
+ return APIError(message, **kwargs)
67
+ if status >= 400:
68
+ return InvalidRequestError(message, **kwargs)
69
+ return APIError(message, **kwargs)
70
+
71
+
72
+ def connection_error(cause: BaseException, message: Optional[str] = None) -> APIConnectionError:
73
+ msg = message or str(cause) or "network request failed"
74
+ err = APIConnectionError(msg)
75
+ err.__cause__ = cause
76
+ return err
77
+
78
+
79
+ def _parse_body(body: Any) -> dict[str, Any]:
80
+ if isinstance(body, dict):
81
+ return body
82
+ if isinstance(body, (bytes, bytearray)):
83
+ try:
84
+ return json.loads(body.decode("utf-8"))
85
+ except Exception:
86
+ return {"message": body.decode("utf-8", errors="replace")}
87
+ if isinstance(body, str):
88
+ try:
89
+ return json.loads(body)
90
+ except Exception:
91
+ return {"message": body}
92
+ return {}
93
+
94
+
95
+ def _header(headers: Mapping[str, str], key: str) -> Optional[str]:
96
+ # httpx headers are case-insensitive but expose .get with the lowercase form.
97
+ if hasattr(headers, "get"):
98
+ try:
99
+ return headers.get(key) or headers.get(key.title())
100
+ except Exception:
101
+ return None
102
+ return None
@@ -0,0 +1,31 @@
1
+ """HTTP header helpers — user-agent, idempotency keys, structured client metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import platform
7
+ import sys
8
+ import uuid
9
+
10
+ from ..version import SDK_VERSION
11
+
12
+
13
+ def user_agent() -> str:
14
+ return f"Transcodely/{SDK_VERSION} python/{sys.version_info.major}.{sys.version_info.minor}"
15
+
16
+
17
+ def client_user_agent_json() -> str:
18
+ return json.dumps(
19
+ {
20
+ "lang": "python",
21
+ "lang_version": platform.python_version(),
22
+ "platform": f"{platform.system()} {platform.release()} {platform.machine()}",
23
+ "publisher": "transcodely",
24
+ "version": SDK_VERSION,
25
+ },
26
+ separators=(",", ":"),
27
+ )
28
+
29
+
30
+ def new_idempotency_key() -> str:
31
+ return str(uuid.uuid4())