transcodely 0.3.1__py3-none-any.whl → 0.3.3__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 (48) hide show
  1. transcodely/__init__.py +41 -21
  2. transcodely/_codec/json_codec.py +2 -3
  3. transcodely/_transport/error_mapping.py +8 -7
  4. transcodely/_transport/transport.py +29 -25
  5. transcodely/client.py +38 -21
  6. transcodely/errors.py +12 -12
  7. transcodely/pagination.py +8 -8
  8. transcodely/resources/_helpers.py +6 -5
  9. transcodely/resources/api_keys.py +6 -6
  10. transcodely/resources/apps.py +53 -8
  11. transcodely/resources/billing.py +90 -0
  12. transcodely/resources/events.py +10 -10
  13. transcodely/resources/health.py +1 -3
  14. transcodely/resources/jobs.py +57 -29
  15. transcodely/resources/memberships.py +9 -8
  16. transcodely/resources/organizations.py +6 -6
  17. transcodely/resources/origins.py +6 -6
  18. transcodely/resources/presets.py +7 -7
  19. transcodely/resources/users.py +9 -8
  20. transcodely/resources/videos.py +48 -8
  21. transcodely/resources/webhook_endpoints.py +32 -31
  22. transcodely/streaming.py +3 -2
  23. transcodely/types.py +260 -182
  24. transcodely/v1/app_pb2.py +52 -38
  25. transcodely/v1/app_pb2.pyi +48 -4
  26. transcodely/v1/billing_pb2.py +65 -0
  27. transcodely/v1/billing_pb2.pyi +131 -0
  28. transcodely/v1/job_pb2.py +86 -65
  29. transcodely/v1/job_pb2.pyi +74 -10
  30. transcodely/v1/subtitles_pb2.py +13 -7
  31. transcodely/v1/subtitles_pb2.pyi +60 -3
  32. transcodely/v1/thumbnails_pb2.py +14 -8
  33. transcodely/v1/thumbnails_pb2.pyi +14 -4
  34. transcodely/v1/video_pb2.py +106 -64
  35. transcodely/v1/video_pb2.pyi +168 -10
  36. transcodely/v1/watermark_pb2.py +57 -0
  37. transcodely/v1/watermark_pb2.pyi +56 -0
  38. transcodely/v1/webhook_pb2.py +68 -64
  39. transcodely/v1/webhook_pb2.pyi +22 -6
  40. transcodely/version.py +1 -1
  41. transcodely/webhooks/construct_event.py +5 -4
  42. transcodely/webhooks/signature.py +6 -6
  43. transcodely/webhooks/types.py +34 -9
  44. {transcodely-0.3.1.dist-info → transcodely-0.3.3.dist-info}/METADATA +87 -4
  45. transcodely-0.3.3.dist-info/RECORD +91 -0
  46. transcodely-0.3.1.dist-info/RECORD +0 -86
  47. {transcodely-0.3.1.dist-info → transcodely-0.3.3.dist-info}/WHEEL +0 -0
  48. {transcodely-0.3.1.dist-info → transcodely-0.3.3.dist-info}/licenses/LICENSE +0 -0
transcodely/__init__.py CHANGED
@@ -47,19 +47,6 @@ from .errors import (
47
47
  WebhookTimestampError,
48
48
  )
49
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
50
 
64
51
  # Headline message classes — most callers reach for these. Everything else lives
65
52
  # in :mod:`transcodely.types`.
@@ -68,11 +55,18 @@ from .types import (
68
55
  App,
69
56
  AudioTrackConfig,
70
57
  AutoABRConfig,
58
+ ChapterPoint,
59
+ ChapterResult,
60
+ ClipConfig,
71
61
  ContentAwareConfig,
72
62
  DASHConfig,
73
63
  DRMConfig,
74
64
  HDRConfig,
75
65
  HLSConfig,
66
+ Invoice,
67
+ InvoiceLineItem,
68
+ InvoiceLineType,
69
+ InvoiceStatus,
76
70
  Job,
77
71
  JobOutput,
78
72
  JobPriority,
@@ -87,6 +81,7 @@ from .types import (
87
81
  PresetVariant,
88
82
  Resolution,
89
83
  StreamingConfig,
84
+ SubtitleResult,
90
85
  SubtitleTrack,
91
86
  ThumbnailResult,
92
87
  ThumbnailSpec,
@@ -95,16 +90,36 @@ from .types import (
95
90
  Video,
96
91
  VideoCodec,
97
92
  VideoRendition,
93
+ VideoTextTrack,
98
94
  VideoVariant,
99
95
  WatchEventType,
100
96
  WatchJobResponse,
101
97
  WatchVideoResponse,
98
+ WatermarkConfig,
102
99
  WebhookDelivery,
103
100
  WebhookEndpoint,
104
101
  )
102
+ from .version import API_VERSION, DEFAULT_BASE_URL, SDK_VERSION
103
+ from .webhooks import (
104
+ DEFAULT_TOLERANCE_SECONDS,
105
+ EVENT_ID_HEADER,
106
+ SIGNATURE_HEADER,
107
+ Event,
108
+ EventRequest,
109
+ EventType,
110
+ WebhookEvent,
111
+ Webhooks,
112
+ construct_event,
113
+ verify_signature,
114
+ )
105
115
 
106
116
  __all__ = [
107
117
  "API_VERSION",
118
+ "DEFAULT_BASE_URL",
119
+ "DEFAULT_TOLERANCE_SECONDS",
120
+ "EVENT_ID_HEADER",
121
+ "SDK_VERSION",
122
+ "SIGNATURE_HEADER",
108
123
  "APIConnectionError",
109
124
  "APIError",
110
125
  "APIKey",
@@ -112,21 +127,24 @@ __all__ = [
112
127
  "AudioTrackConfig",
113
128
  "AuthenticationError",
114
129
  "AutoABRConfig",
130
+ "ChapterPoint",
131
+ "ChapterResult",
132
+ "ClipConfig",
115
133
  "ConflictError",
116
- "construct_event",
117
134
  "ContentAwareConfig",
118
135
  "DASHConfig",
119
- "DEFAULT_BASE_URL",
120
- "DEFAULT_TOLERANCE_SECONDS",
121
136
  "DRMConfig",
122
137
  "Event",
123
- "EVENT_ID_HEADER",
124
138
  "EventRequest",
125
139
  "EventType",
126
140
  "FieldViolation",
127
141
  "HDRConfig",
128
142
  "HLSConfig",
129
143
  "InvalidRequestError",
144
+ "Invoice",
145
+ "InvoiceLineItem",
146
+ "InvoiceLineType",
147
+ "InvoiceStatus",
130
148
  "Job",
131
149
  "JobOutput",
132
150
  "JobPriority",
@@ -146,9 +164,8 @@ __all__ = [
146
164
  "PresetVariant",
147
165
  "RateLimitError",
148
166
  "Resolution",
149
- "SDK_VERSION",
150
- "SIGNATURE_HEADER",
151
167
  "StreamingConfig",
168
+ "SubtitleResult",
152
169
  "SubtitleTrack",
153
170
  "ThumbnailResult",
154
171
  "ThumbnailSpec",
@@ -156,20 +173,23 @@ __all__ = [
156
173
  "TranscodelyError",
157
174
  "User",
158
175
  "UserWithOrganizations",
159
- "verify_signature",
160
176
  "Video",
161
177
  "VideoCodec",
162
178
  "VideoRendition",
179
+ "VideoTextTrack",
163
180
  "VideoVariant",
164
181
  "WatchEventType",
165
182
  "WatchJobResponse",
166
183
  "WatchVideoResponse",
184
+ "WatermarkConfig",
167
185
  "WebhookDelivery",
168
186
  "WebhookEndpoint",
169
187
  "WebhookError",
170
188
  "WebhookEvent",
171
189
  "WebhookPayloadError",
172
- "Webhooks",
173
190
  "WebhookSignatureError",
174
191
  "WebhookTimestampError",
192
+ "Webhooks",
193
+ "construct_event",
194
+ "verify_signature",
175
195
  ]
@@ -132,9 +132,8 @@ def _transform_field(
132
132
  parent[key] = _apply_enum(v, field.enum_type, mode)
133
133
  return
134
134
 
135
- if field.type == FieldDescriptor.TYPE_MESSAGE:
136
- if isinstance(v, dict):
137
- transform_enums_in_dict(v, field.message_type, mode=mode)
135
+ if field.type == FieldDescriptor.TYPE_MESSAGE and isinstance(v, dict):
136
+ transform_enums_in_dict(v, field.message_type, mode=mode)
138
137
 
139
138
 
140
139
  def _apply_enum(value: str, enum_desc: EnumDescriptor, mode: str) -> str:
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from typing import Any, Mapping, Optional
6
+ from collections.abc import Mapping
7
+ from typing import Any
7
8
 
8
9
  from ..errors import (
9
10
  APIConnectionError,
@@ -55,7 +56,7 @@ def map_http_error(*, status: int, body: Any, headers: Mapping[str, str]) -> Tra
55
56
  return InvalidRequestError(message, **kwargs)
56
57
  if status == 429:
57
58
  retry_after = _header(headers, "retry-after")
58
- retry_after_ms: Optional[int] = None
59
+ retry_after_ms: int | None = None
59
60
  if retry_after:
60
61
  try:
61
62
  retry_after_ms = max(0, int(retry_after) * 1000)
@@ -69,7 +70,7 @@ def map_http_error(*, status: int, body: Any, headers: Mapping[str, str]) -> Tra
69
70
  return APIError(message, **kwargs)
70
71
 
71
72
 
72
- def connection_error(cause: BaseException, message: Optional[str] = None) -> APIConnectionError:
73
+ def connection_error(cause: BaseException, message: str | None = None) -> APIConnectionError:
73
74
  msg = message or str(cause) or "network request failed"
74
75
  err = APIConnectionError(msg)
75
76
  err.__cause__ = cause
@@ -82,21 +83,21 @@ def _parse_body(body: Any) -> dict[str, Any]:
82
83
  if isinstance(body, (bytes, bytearray)):
83
84
  try:
84
85
  return json.loads(body.decode("utf-8"))
85
- except Exception:
86
+ except (UnicodeDecodeError, json.JSONDecodeError):
86
87
  return {"message": body.decode("utf-8", errors="replace")}
87
88
  if isinstance(body, str):
88
89
  try:
89
90
  return json.loads(body)
90
- except Exception:
91
+ except json.JSONDecodeError:
91
92
  return {"message": body}
92
93
  return {}
93
94
 
94
95
 
95
- def _header(headers: Mapping[str, str], key: str) -> Optional[str]:
96
+ def _header(headers: Mapping[str, str], key: str) -> str | None:
96
97
  # httpx headers are case-insensitive but expose .get with the lowercase form.
97
98
  if hasattr(headers, "get"):
98
99
  try:
99
100
  return headers.get(key) or headers.get(key.title())
100
- except Exception:
101
+ except Exception: # noqa: BLE001 — headers may be any duck-typed mapping
101
102
  return None
102
103
  return None
@@ -19,11 +19,13 @@ from __future__ import annotations
19
19
  import random
20
20
  import struct
21
21
  import time
22
+ from collections.abc import Callable, Generator, Iterable
22
23
  from dataclasses import dataclass, field
23
- from typing import Any, Callable, Generator, Iterable, Optional, TypeVar
24
+ from typing import Any, TypeVar
24
25
 
25
26
  import httpx
26
27
  from google.protobuf.message import Message
28
+ from typing_extensions import Self
27
29
 
28
30
  from .._codec import json_codec
29
31
  from ..errors import RateLimitError, TranscodelyError
@@ -39,10 +41,10 @@ TRes = TypeVar("TRes", bound=Message)
39
41
  class CallOptions:
40
42
  """Per-call overrides for timeout, retries, idempotency, headers."""
41
43
 
42
- timeout: Optional[float] = None
43
- max_retries: Optional[int] = None
44
- idempotency_key: Optional[str] = None
45
- api_version: Optional[str] = None
44
+ timeout: float | None = None
45
+ max_retries: int | None = None
46
+ idempotency_key: str | None = None
47
+ api_version: str | None = None
46
48
  headers: dict[str, str] = field(default_factory=dict)
47
49
 
48
50
 
@@ -52,9 +54,9 @@ class LogEvent:
52
54
  method: str
53
55
  duration_ms: int
54
56
  attempt: int
55
- status: Optional[int] = None
56
- request_id: Optional[str] = None
57
- error: Optional[TranscodelyError] = None
57
+ status: int | None = None
58
+ request_id: str | None = None
59
+ error: TranscodelyError | None = None
58
60
 
59
61
 
60
62
  _WRITE_PREFIXES = ("Create", "Update", "Delete", "Cancel", "Confirm", "Revoke", "Archive", "Enable")
@@ -71,17 +73,19 @@ class Transport:
71
73
  self,
72
74
  api_key: str,
73
75
  *,
74
- base_url: Optional[str] = None,
76
+ organization_id: str | None = None,
77
+ base_url: str | None = None,
75
78
  timeout: float = 30.0,
76
79
  max_retries: int = 3,
77
- api_version: Optional[str] = None,
78
- default_headers: Optional[dict[str, str]] = None,
79
- http_client: Optional[httpx.Client] = None,
80
- logger: Optional[Callable[[LogEvent], None]] = None,
80
+ api_version: str | None = None,
81
+ default_headers: dict[str, str] | None = None,
82
+ http_client: httpx.Client | None = None,
83
+ logger: Callable[[LogEvent], None] | None = None,
81
84
  ) -> None:
82
85
  if not api_key:
83
86
  raise ValueError("Transcodely: api_key is required")
84
87
  self.api_key = api_key
88
+ self.organization_id = organization_id
85
89
  self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
86
90
  self.timeout = timeout
87
91
  self.max_retries = max_retries
@@ -91,16 +95,16 @@ class Transport:
91
95
  self._owns_client = http_client is None
92
96
  self._logger = logger
93
97
  #: ID of the most recent successful or failed request, Stripe-style.
94
- self.last_request_id: Optional[str] = None
98
+ self.last_request_id: str | None = None
95
99
 
96
100
  def close(self) -> None:
97
101
  if self._owns_client:
98
102
  self._client.close()
99
103
 
100
- def __enter__(self) -> "Transport":
104
+ def __enter__(self) -> Self:
101
105
  return self
102
106
 
103
- def __exit__(self, *args: Any) -> None:
107
+ def __exit__(self, *args: object) -> None:
104
108
  self.close()
105
109
 
106
110
  # ---------- unary ----------
@@ -111,7 +115,7 @@ class Transport:
111
115
  method_name: str,
112
116
  request: Message,
113
117
  response: TRes,
114
- opts: Optional[CallOptions] = None,
118
+ opts: CallOptions | None = None,
115
119
  ) -> TRes:
116
120
  opts = opts or CallOptions()
117
121
  url = f"{self.base_url}/{service_name}/{method_name}"
@@ -161,7 +165,7 @@ class Transport:
161
165
  method_name: str,
162
166
  request: Message,
163
167
  response_factory: Callable[[], TRes],
164
- opts: Optional[CallOptions] = None,
168
+ opts: CallOptions | None = None,
165
169
  ) -> Generator[TRes, None, None]:
166
170
  opts = opts or CallOptions()
167
171
  url = f"{self.base_url}/{service_name}/{method_name}"
@@ -207,7 +211,7 @@ class Transport:
207
211
  self,
208
212
  opts: CallOptions,
209
213
  content_type: str,
210
- idempotency_key: Optional[str] = None,
214
+ idempotency_key: str | None = None,
211
215
  ) -> dict[str, str]:
212
216
  headers = {
213
217
  "content-type": content_type,
@@ -218,6 +222,8 @@ class Transport:
218
222
  "transcodely-version": opts.api_version or self.api_version,
219
223
  "accept": content_type,
220
224
  }
225
+ if self.organization_id:
226
+ headers["x-organization-id"] = self.organization_id
221
227
  if idempotency_key:
222
228
  headers["idempotency-key"] = idempotency_key
223
229
  headers.update(self.default_headers)
@@ -228,7 +234,7 @@ class Transport:
228
234
  if self._logger is not None:
229
235
  try:
230
236
  self._logger(event)
231
- except Exception:
237
+ except Exception: # noqa: BLE001, S110 — a caller's logger must never break requests
232
238
  pass
233
239
 
234
240
  def _with_retry(
@@ -239,7 +245,7 @@ class Transport:
239
245
  fn: Callable[[int], TRes],
240
246
  ) -> TRes:
241
247
  max_retries = opts.max_retries if opts.max_retries is not None else self.max_retries
242
- last_error: Optional[BaseException] = None
248
+ last_error: BaseException | None = None
243
249
  for attempt in range(1, max_retries + 2):
244
250
  try:
245
251
  return fn(attempt)
@@ -260,9 +266,7 @@ def _is_retryable(exc: BaseException) -> bool:
260
266
  return True
261
267
  if exc.__class__.__name__ == "APIConnectionError":
262
268
  return True
263
- if exc.http_status is not None and exc.http_status >= 500:
264
- return True
265
- return False
269
+ return exc.http_status is not None and exc.http_status >= 500
266
270
 
267
271
 
268
272
  def _sleep_backoff(attempt: int, exc: BaseException) -> None:
@@ -308,5 +312,5 @@ def _try_json(data: bytes) -> Any:
308
312
  import json
309
313
 
310
314
  return json.loads(data.decode("utf-8"))
311
- except Exception:
315
+ except (UnicodeDecodeError, json.JSONDecodeError):
312
316
  return data.decode("utf-8", errors="replace")
transcodely/client.py CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Callable, Optional
5
+ from collections.abc import Callable
6
6
 
7
7
  import httpx
8
+ from typing_extensions import Self
8
9
 
9
10
  from ._transport.transport import LogEvent, Transport
10
11
  from .resources.api_keys import ApiKeys
11
12
  from .resources.apps import Apps
13
+ from .resources.billing import Billing
12
14
  from .resources.events import Events
13
15
  from .resources.health import Health
14
16
  from .resources.jobs import Jobs
@@ -37,16 +39,18 @@ class Transcodely:
37
39
  self,
38
40
  api_key: str,
39
41
  *,
40
- base_url: Optional[str] = None,
42
+ organization_id: str | None = None,
43
+ base_url: str | None = None,
41
44
  timeout: float = 30.0,
42
45
  max_retries: int = 3,
43
- api_version: Optional[str] = None,
44
- default_headers: Optional[dict[str, str]] = None,
45
- http_client: Optional[httpx.Client] = None,
46
- logger: Optional[Callable[[LogEvent], None]] = None,
46
+ api_version: str | None = None,
47
+ default_headers: dict[str, str] | None = None,
48
+ http_client: httpx.Client | None = None,
49
+ logger: Callable[[LogEvent], None] | None = None,
47
50
  ) -> None:
48
51
  self._transport = Transport(
49
52
  api_key,
53
+ organization_id=organization_id,
50
54
  base_url=base_url,
51
55
  timeout=timeout,
52
56
  max_retries=max_retries,
@@ -55,30 +59,31 @@ class Transcodely:
55
59
  http_client=http_client,
56
60
  logger=logger,
57
61
  )
58
- self._jobs: Optional[Jobs] = None
59
- self._videos: Optional[Videos] = None
60
- self._presets: Optional[Presets] = None
61
- self._origins: Optional[Origins] = None
62
- self._apps: Optional[Apps] = None
63
- self._api_keys: Optional[ApiKeys] = None
64
- self._organizations: Optional[Organizations] = None
65
- self._memberships: Optional[Memberships] = None
66
- self._users: Optional[Users] = None
67
- self._health: Optional[Health] = None
68
- self._webhook_endpoints: Optional[WebhookEndpoints] = None
69
- self._events: Optional[Events] = None
62
+ self._jobs: Jobs | None = None
63
+ self._videos: Videos | None = None
64
+ self._presets: Presets | None = None
65
+ self._origins: Origins | None = None
66
+ self._apps: Apps | None = None
67
+ self._api_keys: ApiKeys | None = None
68
+ self._organizations: Organizations | None = None
69
+ self._memberships: Memberships | None = None
70
+ self._users: Users | None = None
71
+ self._health: Health | None = None
72
+ self._webhook_endpoints: WebhookEndpoints | None = None
73
+ self._events: Events | None = None
74
+ self._billing: Billing | None = None
70
75
 
71
76
  def close(self) -> None:
72
77
  self._transport.close()
73
78
 
74
- def __enter__(self) -> "Transcodely":
79
+ def __enter__(self) -> Self:
75
80
  return self
76
81
 
77
- def __exit__(self, *args: Any) -> None:
82
+ def __exit__(self, *args: object) -> None:
78
83
  self.close()
79
84
 
80
85
  @property
81
- def last_request_id(self) -> Optional[str]:
86
+ def last_request_id(self) -> str | None:
82
87
  """Stripe-style: ID of the most recent successful or failed request."""
83
88
  return self._transport.last_request_id
84
89
 
@@ -154,6 +159,18 @@ class Transcodely:
154
159
  self._events = Events(self._transport)
155
160
  return self._events
156
161
 
162
+ @property
163
+ def billing(self) -> Billing:
164
+ """The organization's invoices.
165
+
166
+ Needs a dashboard session token for an organization owner plus
167
+ ``organization_id`` on the client; API keys are rejected. See
168
+ :class:`~transcodely.resources.billing.Billing`.
169
+ """
170
+ if self._billing is None:
171
+ self._billing = Billing(self._transport)
172
+ return self._billing
173
+
157
174
  @property
158
175
  def webhooks(self) -> type[Webhooks]:
159
176
  """Stripe-style facade: ``client.webhooks.construct_event(body, sig, secret)``."""
transcodely/errors.py CHANGED
@@ -8,7 +8,7 @@ error handling. Mirrors the TypeScript and Go SDK error classes.
8
8
  from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass
11
- from typing import Any, Optional
11
+ from typing import Any
12
12
 
13
13
 
14
14
  @dataclass(frozen=True)
@@ -26,19 +26,19 @@ class TranscodelyError(Exception):
26
26
  self,
27
27
  message: str,
28
28
  *,
29
- code: Optional[str] = None,
30
- type: Optional[str] = None,
31
- errors: Optional[list[FieldViolation]] = None,
32
- http_status: Optional[int] = None,
33
- request_id: Optional[str] = None,
29
+ code: str | None = None,
30
+ type: str | None = None,
31
+ errors: list[FieldViolation] | None = None,
32
+ http_status: int | None = None,
33
+ request_id: str | None = None,
34
34
  raw: Any = None,
35
35
  ) -> None:
36
36
  super().__init__(message)
37
- self.code: Optional[str] = code
38
- self.type: Optional[str] = type
37
+ self.code: str | None = code
38
+ self.type: str | None = type
39
39
  self.errors: list[FieldViolation] = errors or []
40
- self.http_status: Optional[int] = http_status
41
- self.request_id: Optional[str] = request_id
40
+ self.http_status: int | None = http_status
41
+ self.request_id: str | None = request_id
42
42
  self.raw: Any = raw
43
43
 
44
44
  def __repr__(self) -> str:
@@ -79,11 +79,11 @@ class RateLimitError(TranscodelyError):
79
79
  self,
80
80
  message: str,
81
81
  *,
82
- retry_after_ms: Optional[int] = None,
82
+ retry_after_ms: int | None = None,
83
83
  **kwargs: Any,
84
84
  ) -> None:
85
85
  super().__init__(message, **kwargs)
86
- self.retry_after_ms: Optional[int] = retry_after_ms
86
+ self.retry_after_ms: int | None = retry_after_ms
87
87
 
88
88
 
89
89
  class InvalidRequestError(TranscodelyError):
transcodely/pagination.py CHANGED
@@ -6,8 +6,9 @@ Every ``list`` method returns a :class:`Page` you can iterate manually or via
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ from collections.abc import Callable, Generator, Iterator
9
10
  from dataclasses import dataclass
10
- from typing import Callable, Generator, Generic, Iterator, Optional, TypeVar
11
+ from typing import Generic, TypeVar
11
12
 
12
13
  T = TypeVar("T")
13
14
 
@@ -15,22 +16,22 @@ T = TypeVar("T")
15
16
  @dataclass
16
17
  class PageContents(Generic[T]):
17
18
  items: list[T]
18
- next_cursor: Optional[str]
19
+ next_cursor: str | None
19
20
 
20
21
 
21
22
  class Page(Generic[T]):
22
23
  """A single page of results plus an opt-in iterator across all pages."""
23
24
 
24
- def __init__(self, fetcher: Callable[[Optional[str]], PageContents[T]]) -> None:
25
+ def __init__(self, fetcher: Callable[[str | None], PageContents[T]]) -> None:
25
26
  self._fetcher = fetcher
26
- self._first: Optional[PageContents[T]] = None
27
+ self._first: PageContents[T] | None = None
27
28
 
28
29
  @property
29
30
  def items(self) -> list[T]:
30
31
  return self._first_page().items
31
32
 
32
33
  @property
33
- def next_cursor(self) -> Optional[str]:
34
+ def next_cursor(self) -> str | None:
34
35
  return self._first_page().next_cursor
35
36
 
36
37
  def __iter__(self) -> Iterator[T]:
@@ -38,11 +39,10 @@ class Page(Generic[T]):
38
39
 
39
40
  def auto_paging_iter(self) -> Generator[T, None, None]:
40
41
  """Yield every item, transparently fetching subsequent pages."""
41
- cursor: Optional[str] = None
42
+ cursor: str | None = None
42
43
  while True:
43
44
  page = self._fetcher(cursor)
44
- for item in page.items:
45
- yield item
45
+ yield from page.items
46
46
  if not page.next_cursor:
47
47
  return
48
48
  cursor = page.next_cursor
@@ -2,7 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Mapping, Optional, Union
5
+ from collections.abc import Mapping
6
+ from typing import Any
6
7
 
7
8
  from google.protobuf import json_format
8
9
  from google.protobuf.descriptor import EnumDescriptor
@@ -23,7 +24,7 @@ def fill_from_dict(msg: Message, data: Mapping[str, Any]) -> Message:
23
24
  return msg
24
25
 
25
26
 
26
- def resolve_enum(value: Union[int, str], enum_desc: EnumDescriptor) -> int:
27
+ def resolve_enum(value: int | str, enum_desc: EnumDescriptor) -> int:
27
28
  """Resolve an enum value from either the raw proto int or a string.
28
29
 
29
30
  Strings may be the simplified lowercase wire form (``"standard"``,
@@ -54,9 +55,9 @@ def resolve_enum(value: Union[int, str], enum_desc: EnumDescriptor) -> int:
54
55
  def assign_pagination(
55
56
  req_pagination: Any,
56
57
  *,
57
- limit: Optional[int] = None,
58
- cursor: Optional[str] = None,
59
- offset: Optional[int] = None,
58
+ limit: int | None = None,
59
+ cursor: str | None = None,
60
+ offset: int | None = None,
60
61
  ) -> None:
61
62
  if limit is not None:
62
63
  req_pagination.limit = limit
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from .._transport.transport import CallOptions, Transport
8
8
  from ..pagination import Page, PageContents
@@ -21,17 +21,17 @@ class ApiKeys:
21
21
  req = fill_from_dict(api_key_pb2.CreateAPIKeyRequest(), kwargs)
22
22
  return self._t.unary(_SERVICE, "Create", req, api_key_pb2.CreateAPIKeyResponse())
23
23
 
24
- def get(self, id: str, opts: Optional[CallOptions] = None) -> api_key_pb2.APIKey:
24
+ def get(self, id: str, opts: CallOptions | None = None) -> api_key_pb2.APIKey:
25
25
  req = api_key_pb2.GetAPIKeyRequest(id=id)
26
26
  return self._t.unary(_SERVICE, "Get", req, api_key_pb2.GetAPIKeyResponse(), opts).api_key
27
27
 
28
28
  def list(
29
29
  self,
30
30
  *,
31
- limit: Optional[int] = None,
32
- opts: Optional[CallOptions] = None,
31
+ limit: int | None = None,
32
+ opts: CallOptions | None = None,
33
33
  ) -> Page[api_key_pb2.APIKey]:
34
- def fetch(cursor: Optional[str]) -> PageContents[api_key_pb2.APIKey]:
34
+ def fetch(cursor: str | None) -> PageContents[api_key_pb2.APIKey]:
35
35
  req = api_key_pb2.ListAPIKeysRequest()
36
36
  assign_pagination(req.pagination, limit=limit, cursor=cursor)
37
37
  res = self._t.unary(_SERVICE, "List", req, api_key_pb2.ListAPIKeysResponse(), opts)
@@ -41,7 +41,7 @@ class ApiKeys:
41
41
 
42
42
  return Page(fetch)
43
43
 
44
- def revoke(self, id: str, opts: Optional[CallOptions] = None) -> api_key_pb2.APIKey:
44
+ def revoke(self, id: str, opts: CallOptions | None = None) -> api_key_pb2.APIKey:
45
45
  req = api_key_pb2.RevokeAPIKeyRequest(id=id)
46
46
  return self._t.unary(
47
47
  _SERVICE, "Revoke", req, api_key_pb2.RevokeAPIKeyResponse(), opts