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.
- buf/__init__.py +0 -0
- buf/validate/__init__.py +0 -0
- buf/validate/validate_pb2.py +469 -0
- buf/validate/validate_pb2.pyi +651 -0
- transcodely/__init__.py +175 -0
- transcodely/_codec/__init__.py +0 -0
- transcodely/_codec/json_codec.py +193 -0
- transcodely/_transport/__init__.py +0 -0
- transcodely/_transport/error_mapping.py +102 -0
- transcodely/_transport/headers.py +31 -0
- transcodely/_transport/transport.py +312 -0
- transcodely/client.py +160 -0
- transcodely/errors.py +115 -0
- transcodely/pagination.py +53 -0
- transcodely/py.typed +0 -0
- transcodely/resources/__init__.py +0 -0
- transcodely/resources/_helpers.py +29 -0
- transcodely/resources/api_keys.py +48 -0
- transcodely/resources/apps.py +59 -0
- transcodely/resources/events.py +118 -0
- transcodely/resources/health.py +19 -0
- transcodely/resources/jobs.py +120 -0
- transcodely/resources/memberships.py +54 -0
- transcodely/resources/organizations.py +73 -0
- transcodely/resources/origins.py +55 -0
- transcodely/resources/presets.py +63 -0
- transcodely/resources/users.py +46 -0
- transcodely/resources/videos.py +114 -0
- transcodely/resources/webhook_endpoints.py +242 -0
- transcodely/streaming.py +47 -0
- transcodely/types.py +570 -0
- transcodely/v1/api_key_pb2.py +69 -0
- transcodely/v1/api_key_pb2.pyi +97 -0
- transcodely/v1/app_pb2.py +107 -0
- transcodely/v1/app_pb2.pyi +219 -0
- transcodely/v1/codec_av1_pb2.py +55 -0
- transcodely/v1/codec_av1_pb2.pyi +34 -0
- transcodely/v1/codec_h264_pb2.py +71 -0
- transcodely/v1/codec_h264_pb2.pyi +44 -0
- transcodely/v1/codec_h265_pb2.py +69 -0
- transcodely/v1/codec_h265_pb2.pyi +42 -0
- transcodely/v1/codec_vp9_pb2.py +57 -0
- transcodely/v1/codec_vp9_pb2.pyi +30 -0
- transcodely/v1/common_pb2.py +67 -0
- transcodely/v1/common_pb2.pyi +171 -0
- transcodely/v1/content_aware_pb2.py +56 -0
- transcodely/v1/content_aware_pb2.pyi +51 -0
- transcodely/v1/drm_pb2.py +65 -0
- transcodely/v1/drm_pb2.pyi +68 -0
- transcodely/v1/hdr_pb2.py +57 -0
- transcodely/v1/hdr_pb2.pyi +64 -0
- transcodely/v1/health_pb2.py +44 -0
- transcodely/v1/health_pb2.pyi +44 -0
- transcodely/v1/job_pb2.py +201 -0
- transcodely/v1/job_pb2.pyi +546 -0
- transcodely/v1/media_pb2.py +43 -0
- transcodely/v1/media_pb2.pyi +100 -0
- transcodely/v1/membership_pb2.py +71 -0
- transcodely/v1/membership_pb2.pyi +122 -0
- transcodely/v1/organization_pb2.py +81 -0
- transcodely/v1/organization_pb2.pyi +121 -0
- transcodely/v1/origin_pb2.py +157 -0
- transcodely/v1/origin_pb2.pyi +317 -0
- transcodely/v1/preset_pb2.py +192 -0
- transcodely/v1/preset_pb2.pyi +290 -0
- transcodely/v1/streaming_pb2.py +59 -0
- transcodely/v1/streaming_pb2.pyi +56 -0
- transcodely/v1/subtitles_pb2.py +69 -0
- transcodely/v1/subtitles_pb2.pyi +75 -0
- transcodely/v1/thumbnails_pb2.py +63 -0
- transcodely/v1/thumbnails_pb2.pyi +74 -0
- transcodely/v1/user_pb2.py +72 -0
- transcodely/v1/user_pb2.pyi +137 -0
- transcodely/v1/video_pb2.py +166 -0
- transcodely/v1/video_pb2.pyi +367 -0
- transcodely/v1/webhook_pb2.py +151 -0
- transcodely/v1/webhook_pb2.pyi +336 -0
- transcodely/version.py +11 -0
- transcodely/webhooks/__init__.py +44 -0
- transcodely/webhooks/construct_event.py +111 -0
- transcodely/webhooks/signature.py +110 -0
- transcodely/webhooks/types.py +115 -0
- transcodely-0.2.0.dist-info/METADATA +349 -0
- transcodely-0.2.0.dist-info/RECORD +86 -0
- transcodely-0.2.0.dist-info/WHEEL +4 -0
- transcodely-0.2.0.dist-info/licenses/LICENSE +21 -0
transcodely/__init__.py
ADDED
|
@@ -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())
|