sg-python-api 0.1.2__py3-none-any.whl → 0.2.1__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 (56) hide show
  1. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.1.dist-info}/METADATA +21 -24
  2. sg_python_api-0.2.1.dist-info/RECORD +58 -0
  3. shotgun_api3/__init__.py +4 -0
  4. shotgun_api3/_async_client.py +347 -281
  5. shotgun_api3/_async_driver.py +25 -55
  6. shotgun_api3/_async_httpx2.py +92 -59
  7. shotgun_api3/_async_transport.py +38 -3
  8. shotgun_api3/_capabilities.py +4 -3
  9. shotgun_api3/_client.py +296 -422
  10. shotgun_api3/_config.py +245 -120
  11. shotgun_api3/_connections.py +144 -0
  12. shotgun_api3/_crud.py +25 -250
  13. shotgun_api3/_driver.py +29 -54
  14. shotgun_api3/_execution.py +1 -3
  15. shotgun_api3/_files.py +193 -21
  16. shotgun_api3/_methods.py +59 -411
  17. shotgun_api3/_protocol.py +27 -53
  18. shotgun_api3/_transport.py +82 -407
  19. shotgun_api3/_version.py +7 -2
  20. shotgun_api3/lib/mockgun/__init__.py +20 -7
  21. shotgun_api3/lib/mockgun/_activity.py +510 -0
  22. shotgun_api3/lib/mockgun/_async_client.py +196 -0
  23. shotgun_api3/lib/mockgun/_dates.py +164 -0
  24. shotgun_api3/lib/mockgun/_errors.py +18 -0
  25. shotgun_api3/lib/mockgun/_events.py +1052 -0
  26. shotgun_api3/lib/mockgun/_follows.py +217 -0
  27. shotgun_api3/lib/mockgun/_handlers.py +553 -0
  28. shotgun_api3/lib/mockgun/_links.py +598 -0
  29. shotgun_api3/lib/mockgun/_new_client.py +92 -0
  30. shotgun_api3/lib/mockgun/_presets.py +492 -0
  31. shotgun_api3/lib/mockgun/_query.py +2205 -0
  32. shotgun_api3/lib/mockgun/_retirement.py +463 -0
  33. shotgun_api3/lib/mockgun/_schema.py +172 -0
  34. shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
  35. shotgun_api3/lib/mockgun/_search.py +378 -0
  36. shotgun_api3/lib/mockgun/_server.py +169 -0
  37. shotgun_api3/lib/mockgun/_sessions.py +206 -0
  38. shotgun_api3/lib/mockgun/_site.py +150 -0
  39. shotgun_api3/lib/mockgun/_site_data.py +584 -0
  40. shotgun_api3/lib/mockgun/_storage.py +208 -0
  41. shotgun_api3/lib/mockgun/_store.py +1559 -0
  42. shotgun_api3/lib/mockgun/_summaries.py +965 -0
  43. shotgun_api3/lib/mockgun/_sync_client.py +190 -0
  44. shotgun_api3/lib/mockgun/_threads.py +226 -0
  45. shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
  46. shotgun_api3/lib/mockgun/_transport.py +145 -0
  47. shotgun_api3/lib/mockgun/_uploads.py +417 -0
  48. shotgun_api3/lib/mockgun/_wire.py +120 -0
  49. shotgun_api3/lib/mockgun/mockgun.py +14 -0
  50. shotgun_api3/lib/sgtimezone.py +24 -1
  51. sg_python_api-0.1.2.dist-info/RECORD +0 -31
  52. shotgun_api3/_signatures.py +0 -53
  53. shotgun_api3/lib/mockgun/_async.py +0 -315
  54. shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
  55. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.1.dist-info}/WHEEL +0 -0
  56. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,13 @@
1
1
  """Native-async Shotgun client and lifecycle."""
2
2
 
3
- # Same-file collaboration follows module-boundary privacy.
4
- # pyright: reportPrivateUsage=false
5
-
6
3
  import asyncio
7
4
  import copy
8
5
  import dataclasses
9
6
  import datetime as _dt_module
10
7
  import importlib
11
- import inspect
12
8
  import os
13
9
  import threading
14
10
  import urllib.request
15
- import uuid
16
11
  from collections.abc import AsyncGenerator, AsyncIterator, Callable, Mapping
17
12
  from contextlib import ExitStack, asynccontextmanager, suppress
18
13
  from pathlib import Path
@@ -21,6 +16,7 @@ from typing import (
21
16
  TYPE_CHECKING,
22
17
  Any,
23
18
  BinaryIO,
19
+ ClassVar,
24
20
  Literal,
25
21
  Never,
26
22
  ParamSpec,
@@ -43,12 +39,8 @@ from ._async_driver import (
43
39
  from ._async_transport import AsyncStreamingResponse, AsyncTransport
44
40
  from ._capabilities import ClientCapabilities, ServerCapabilities
45
41
  from ._client import (
46
- CONSTRUCTOR_SIGNATURE,
47
42
  ConstructorOptions,
48
- Shotgun,
49
- bind_constructor,
50
43
  build_config,
51
- expose_constructor_signature,
52
44
  )
53
45
  from ._config import (
54
46
  Config,
@@ -60,7 +52,6 @@ from ._config import (
60
52
  server_page_limit,
61
53
  )
62
54
  from ._execution import SuppressedExceptionCapture, release_lease, retire_transport
63
- from ._signatures import bind_arguments, expose_signature
64
55
  from ._transport import (
65
56
  ReadTimeoutError,
66
57
  SendError,
@@ -80,11 +71,6 @@ _FilesystemParams = ParamSpec('_FilesystemParams')
80
71
  _FilesystemResult = TypeVar('_FilesystemResult')
81
72
  _UploadResult = TypeVar('_UploadResult')
82
73
 
83
- _METHOD_UPLOAD_SIGNATURE = inspect.signature(Shotgun.upload)
84
- _UPLOAD_SIGNATURE = inspect.signature(Shotgun.upload).replace(
85
- parameters=tuple(inspect.signature(Shotgun.upload).parameters.values())[1:]
86
- )
87
-
88
74
 
89
75
  class _AsyncDownloadCancellationScope:
90
76
  """Own and join every filesystem worker started by one transfer."""
@@ -94,12 +80,17 @@ class _AsyncDownloadCancellationScope:
94
80
  self._captured_cancellation: asyncio.CancelledError | None = None
95
81
 
96
82
  @property
97
- def _cancellation(self) -> asyncio.CancelledError | None:
83
+ def cancellation(self) -> asyncio.CancelledError | None:
98
84
  """Return the first cancellation captured while joining a worker."""
99
85
  return self._captured_cancellation
100
86
 
101
- def _capture_cancellation(self, error: BaseException) -> None:
102
- """Retain one actively delivered caller cancellation."""
87
+ def capture_cancellation(self, error: BaseException) -> None:
88
+ """Retain one actively delivered caller cancellation.
89
+
90
+ Args:
91
+ error: Failure to retain if it is an active caller cancellation;
92
+ other failures are ignored.
93
+ """
103
94
  current = asyncio.current_task()
104
95
  if (
105
96
  not isinstance(error, asyncio.CancelledError)
@@ -111,28 +102,45 @@ class _AsyncDownloadCancellationScope:
111
102
  self._captured_cancellation = error
112
103
  current.uncancel()
113
104
 
114
- def _raise_if_cancelled(self) -> None:
115
- """Propagate a captured cancellation only between worker phases."""
105
+ def raise_if_cancelled(self) -> None:
106
+ """Propagate a captured cancellation only between worker phases.
107
+
108
+ Raises:
109
+ asyncio.CancelledError: If caller cancellation was captured.
110
+ """
116
111
  if self._captured_cancellation is not None:
117
112
  raise self._captured_cancellation
118
113
 
119
- async def _run(
114
+ async def run(
120
115
  self,
121
116
  function: Callable[_FilesystemParams, _FilesystemResult],
122
117
  *args: _FilesystemParams.args,
123
118
  **kwargs: _FilesystemParams.kwargs,
124
119
  ) -> _FilesystemResult:
125
- """Run one worker to completion while retaining cancellation."""
120
+ """Run one worker to completion while retaining cancellation.
121
+
122
+ Args:
123
+ function: Blocking operation to execute off-loop.
124
+ *args: Positional arguments forwarded to the operation.
125
+ **kwargs: Keyword arguments forwarded to the operation.
126
+
127
+ Returns:
128
+ The operation's result after its worker finishes.
129
+
130
+ Raises:
131
+ BaseException: If the worker fails. A captured caller cancellation
132
+ takes precedence over the worker's failure.
133
+ """
126
134
  task = asyncio.create_task(asyncio.to_thread(function, *args, **kwargs))
127
135
  while not task.done():
128
136
  try:
129
137
  await asyncio.wait({task})
130
138
  except asyncio.CancelledError as error:
131
- self._capture_cancellation(error)
139
+ self.capture_cancellation(error)
132
140
  try:
133
141
  return task.result()
134
142
  except BaseException:
135
- self._raise_if_cancelled()
143
+ self.raise_if_cancelled()
136
144
  raise
137
145
 
138
146
 
@@ -155,7 +163,7 @@ class _AsyncDownloadCleanupCapture:
155
163
  _traceback: TracebackType | None,
156
164
  ) -> bool:
157
165
  """Capture and suppress one async download-cleanup exception."""
158
- self.exception = self._workers._cancellation or exception
166
+ self.exception = self._workers.cancellation or exception
159
167
  return exception is not None
160
168
 
161
169
 
@@ -193,35 +201,38 @@ class _AsyncDownloadExchange:
193
201
  workers = _AsyncDownloadCancellationScope()
194
202
  try:
195
203
  try:
196
- stack, handle, temporary = await workers._run(
197
- _open_download_temporary,
198
- destination,
199
- )
200
- workers._raise_if_cancelled()
204
+ with _files.download_destination_io(destination):
205
+ stack, handle, temporary = await workers.run(
206
+ _open_download_temporary,
207
+ destination,
208
+ )
209
+ workers.raise_if_cancelled()
201
210
  opened_handle = handle
202
211
 
203
212
  async def write(chunk: bytes) -> None:
204
- await workers._run(opened_handle.write, chunk)
205
- workers._raise_if_cancelled()
213
+ with _files.download_destination_io(destination):
214
+ await workers.run(opened_handle.write, chunk)
215
+ workers.raise_if_cancelled()
206
216
 
207
217
  await control.consume_into(chunks, write, total_bytes=total_bytes)
208
- await workers._run(handle.flush)
209
- workers._raise_if_cancelled()
218
+ with _files.download_destination_io(destination):
219
+ await workers.run(handle.flush)
220
+ workers.raise_if_cancelled()
210
221
  close_exception = await _capture_download_cleanup(workers, stack.close)
211
- stack = None
212
- handle = None
213
- workers._raise_if_cancelled()
222
+ stack, handle = None, None
223
+ workers.raise_if_cancelled()
214
224
  if close_exception is not None:
215
225
  _raise_download_cleanup_error()
216
226
  # This is the final cancellation checkpoint before atomic commit.
217
227
  # Once `replace` starts, its worker is joined before cancellation
218
228
  # propagates, so the destination cannot change after return.
219
229
  await asyncio.sleep(0)
220
- await workers._run(temporary.replace, destination)
221
- workers._raise_if_cancelled()
230
+ with _files.download_destination_io(destination):
231
+ await workers.run(temporary.replace, destination)
232
+ workers.raise_if_cancelled()
222
233
  except BaseException as error:
223
234
  operation_exception = error
224
- workers._capture_cancellation(error)
235
+ workers.capture_cancellation(error)
225
236
  raise
226
237
  finally:
227
238
  close_exception: BaseException | None = None
@@ -234,7 +245,7 @@ class _AsyncDownloadExchange:
234
245
  temporary.unlink,
235
246
  missing_ok=True,
236
247
  )
237
- workers._raise_if_cancelled()
248
+ workers.raise_if_cancelled()
238
249
  if operation_exception is None:
239
250
  if close_exception is not None:
240
251
  raise _AsyncDownloadCleanupError from None
@@ -258,12 +269,23 @@ class _AsyncDownloadExchange:
258
269
  chunks = streaming.aiter_chunks(_ASYNC_FILE_BATCH_SIZE)
259
270
  return await self._write_file(chunks, control, total_bytes)
260
271
 
261
- async def _execute(
272
+ async def execute(
262
273
  self,
263
274
  driver: AsyncDriver,
264
275
  operation: _protocol.OperationDescriptor,
265
276
  ) -> bytes | str:
266
- """Execute the workflow through the common async attempt loop."""
277
+ """Execute the workflow through the common async attempt loop.
278
+
279
+ Returns:
280
+ Downloaded bytes, or the destination path for a file download.
281
+
282
+ Raises:
283
+ SendError: If required download cleanup fails.
284
+ TransportError: If the attempt loop cannot complete the transfer.
285
+ ShotgunFileDownloadError: If the response or redirect is invalid,
286
+ or creating, writing, flushing, or publishing the destination
287
+ file fails.
288
+ """
267
289
  try:
268
290
  return await driver.execute(
269
291
  operation,
@@ -310,36 +332,6 @@ class _TokenExchangeProgress:
310
332
  token: str | None = None
311
333
 
312
334
 
313
- def _bind_upload(
314
- args: tuple[object, ...],
315
- kwargs: dict[str, object],
316
- ) -> _files.UploadArguments:
317
- """Bind runtime arguments with upload-specific errors."""
318
- bound = bind_arguments(_UPLOAD_SIGNATURE, args, kwargs, 'AsyncShotgun.upload()')
319
- arguments = cast('Callable[..., _files.UploadArguments]', _files.UploadArguments)
320
- return arguments(**bound.arguments)
321
-
322
-
323
- async def _capture_download_cleanup(
324
- workers: _AsyncDownloadCancellationScope,
325
- function: Callable[_FilesystemParams, _FilesystemResult],
326
- *args: _FilesystemParams.args,
327
- **kwargs: _FilesystemParams.kwargs,
328
- ) -> BaseException | None:
329
- """Run one joined cleanup operation and capture its failure."""
330
- capture = _AsyncDownloadCleanupCapture(workers)
331
- with capture:
332
- await workers._run(function, *args, **kwargs)
333
- return capture.exception
334
-
335
-
336
- def _expose_upload_signature(function: Callable[..., object]) -> Callable[..., object]:
337
- """Attach the implemented sync upload signature and annotations."""
338
- expose_signature(function, _METHOD_UPLOAD_SIGNATURE, Shotgun.upload.__doc__)
339
- object.__setattr__(function, '__annotations__', Shotgun.upload.__annotations__)
340
- return function
341
-
342
-
343
335
  class AsyncShotgun:
344
336
  """Provide the asynchronous Flow Production Tracking API client surface.
345
337
 
@@ -356,12 +348,8 @@ class AsyncShotgun:
356
348
  server_info: Cached raw metadata for the configured origin.
357
349
  """
358
350
 
359
- __signature__ = CONSTRUCTOR_SIGNATURE
351
+ _in_process: ClassVar[bool] = False
360
352
 
361
- @overload
362
- def __init__(self, base_url: Never) -> None: ...
363
-
364
- @overload
365
353
  def __init__(
366
354
  self,
367
355
  base_url: str,
@@ -376,10 +364,7 @@ class AsyncShotgun:
376
364
  sudo_as_login: str | None = None,
377
365
  session_token: str | None = None,
378
366
  auth_token: str | None = None,
379
- ) -> None: ...
380
-
381
- @expose_constructor_signature
382
- def __init__(self, *args: object, **kwargs: object) -> None:
367
+ ) -> None:
383
368
  """Initialize a client for one site and authentication mode.
384
369
 
385
370
  Supply exactly one authentication mode: `script_name` with `api_key`,
@@ -391,6 +376,8 @@ class AsyncShotgun:
391
376
  `http_proxy` accepts `[user:pass@]host[:port]`, with port 8080 when
392
377
  omitted. A truthy `ca_certs` selects a CA bundle; otherwise
393
378
  `SHOTGUN_API_CACERTS` is consulted before using system trust roots.
379
+ `SHOTGUN_API_RETRY_INTERVAL` supplies a nonnegative integer retry
380
+ interval in milliseconds, defaulting to 3000.
394
381
 
395
382
  Constructor validation is synchronous; readiness and authentication
396
383
  exchange I/O begins only through an operation, `connect()`, or an async
@@ -398,20 +385,57 @@ class AsyncShotgun:
398
385
  I/O.
399
386
 
400
387
  Args:
401
- *args: Positional values from the documented constructor signature.
402
- **kwargs: Keyword values from the documented constructor signature.
388
+ base_url: Flow Production Tracking site URL.
389
+ script_name: Script name for script authentication.
390
+ api_key: API key for script authentication.
391
+ convert_datetimes_to_utc: Whether to convert datetime values to
392
+ UTC.
393
+ http_proxy: Optional proxy authority in `[user:pass@]host[:port]`
394
+ form.
395
+ connect: Whether to request eager readiness during asynchronous
396
+ context entry.
397
+ ca_certs: Optional certificate-authority bundle path.
398
+ login: Login for human-user authentication.
399
+ password: Password for human-user authentication.
400
+ sudo_as_login: Optional user login for sudo impersonation.
401
+ session_token: Existing session token for authentication.
402
+ auth_token: Optional two-factor authentication token.
403
403
 
404
404
  Raises:
405
- TypeError: If arguments do not match the documented signature.
406
- ValueError: If authentication, URL, or proxy values are invalid.
405
+ ValueError: If authentication, URL, or proxy values are invalid,
406
+ the configured CA bundle is not a readable file, or
407
+ `SHOTGUN_API_RETRY_INTERVAL` is not a nonnegative integer.
407
408
  """
408
- self._initialize(bind_constructor(args, kwargs, 'AsyncShotgun'))
409
+ self._initialize(
410
+ ConstructorOptions(
411
+ base_url,
412
+ script_name,
413
+ api_key,
414
+ convert_datetimes_to_utc,
415
+ http_proxy,
416
+ connect,
417
+ ca_certs,
418
+ login,
419
+ password,
420
+ sudo_as_login,
421
+ session_token,
422
+ auth_token,
423
+ )
424
+ )
409
425
 
410
426
  async def __aenter__(self) -> Self:
411
427
  """Enter this client and perform any configured eager connection.
412
428
 
413
429
  Returns:
414
430
  This client.
431
+
432
+ Raises:
433
+ ShotgunError: If eager connection encounters an RPC fault or
434
+ terminal transport failure.
435
+ ProtocolError: If an eager connection request returns an HTTP
436
+ error.
437
+ ValueError: If eager authenticated HTTP violates the configured
438
+ origin policy.
415
439
  """
416
440
  if not self._connect_on_enter:
417
441
  return self
@@ -427,6 +451,10 @@ class AsyncShotgun:
427
451
 
428
452
  Args:
429
453
  *exc: Context-manager exception details, if any.
454
+
455
+ Raises:
456
+ ShotgunError: If the built-in transport cannot finish cleanup.
457
+ Exception: If a replacement transport's cleanup fails.
430
458
  """
431
459
  await self.close()
432
460
 
@@ -482,10 +510,14 @@ class AsyncShotgun:
482
510
  def _initialize(self, options: ConstructorOptions) -> None:
483
511
  """Initialize network-free state from validated constructor options."""
484
512
  self._base_url = options.base_url.rstrip('/')
485
- self._ca_certs = options.ca_certs or os.environ.get('SHOTGUN_API_CACERTS') or None
513
+ self._ca_certs = (
514
+ options.ca_certs or os.environ.get('SHOTGUN_API_CACERTS') or None
515
+ if self._in_process
516
+ else config_module.constructor_ca_certs(options.ca_certs)
517
+ )
486
518
  self._client_caps = ClientCapabilities()
487
519
  self._connect_on_enter = options.connect
488
- self._config = build_config(options)
520
+ self._config = build_config(options, in_process=self._in_process)
489
521
 
490
522
  self._driver: AsyncDriver | None = None
491
523
  self._driver_generation: int | None = None
@@ -546,7 +578,11 @@ class AsyncShotgun:
546
578
  raise cancellation
547
579
 
548
580
  def _new_transport(self, context: RequestContext) -> AsyncTransport:
549
- """Create an httpx2 transport from one immutable context."""
581
+ """Create a transport from one context; mockgun overrides this package-internal hook."""
582
+ if context.in_process:
583
+ raise RuntimeError('an in-process client must supply its own transport')
584
+ if context.pool_maxsize < 1:
585
+ raise ValueError('pool_maxsize must be at least 1')
550
586
  module = importlib.import_module(f'{__package__}._async_httpx2')
551
587
  factory = cast(
552
588
  'type[Httpx2Transport]',
@@ -557,6 +593,8 @@ class AsyncShotgun:
557
593
  proxy_headers=dict(context.proxy_headers) or None,
558
594
  proxy_url=context.proxy_url,
559
595
  pool_maxsize=context.pool_maxsize,
596
+ max_connection_idle_secs=context.max_connection_idle_secs,
597
+ tcp_keepalive_timeout_secs=context.tcp_keepalive_timeout_secs,
560
598
  )
561
599
 
562
600
  def _acquire_driver(
@@ -566,7 +604,14 @@ class AsyncShotgun:
566
604
  """Lease one generation's driver and return idle transports to close."""
567
605
  close_after_unlock: list[AsyncTransport] = []
568
606
  with self._transport_lock:
569
- if self._known_transport is not self._transport:
607
+ decision = _files.choose_driver_lease(
608
+ self._known_transport,
609
+ self._transport,
610
+ self._driver_generation,
611
+ context.transport_generation,
612
+ self._transport_owned,
613
+ )
614
+ if decision.known_changed:
570
615
  if self._known_transport is not None:
571
616
  retired = retire_transport(
572
617
  self._lease_counts, self._retired_transports, self._known_transport
@@ -584,16 +629,14 @@ class AsyncShotgun:
584
629
  self._transport = transport
585
630
  self._known_transport = transport
586
631
  self._transport_owned = True
587
- elif self._driver_generation not in {None, context.transport_generation}:
588
- if self._transport_owned:
589
- retired = retire_transport(
590
- self._lease_counts, self._retired_transports, transport
591
- )
592
- if retired is not None:
593
- close_after_unlock.append(retired)
594
- transport = self._new_transport(context)
595
- self._transport = transport
596
- self._known_transport = transport
632
+ elif decision.replace_owned_transport:
633
+ retired = retire_transport(self._lease_counts, self._retired_transports, transport)
634
+ if retired is not None:
635
+ close_after_unlock.append(retired)
636
+ transport = self._new_transport(context)
637
+ self._transport = transport
638
+ self._known_transport = transport
639
+ if decision.reset_driver:
597
640
  self._driver = None
598
641
 
599
642
  if self._driver is None:
@@ -951,7 +994,9 @@ class AsyncShotgun:
951
994
  driver,
952
995
  context,
953
996
  exchange,
954
- lambda text, _headers: _files.parse_link_completion(text),
997
+ lambda text, _headers: _files.parse_link_completion(
998
+ text, operation='share_thumbnail', entities=entities
999
+ ),
955
1000
  )
956
1001
 
957
1002
  async def _thumbnail_source_ready(
@@ -1025,7 +1070,7 @@ class AsyncShotgun:
1025
1070
  _files.StorageRange(
1026
1071
  length=prepared.file_size,
1027
1072
  offset=0,
1028
- path=prepared.options.arguments.path,
1073
+ path=prepared.path,
1029
1074
  ),
1030
1075
  chunk_size=_ASYNC_FILE_BATCH_SIZE,
1031
1076
  )
@@ -1062,7 +1107,16 @@ class AsyncShotgun:
1062
1107
  driver,
1063
1108
  context,
1064
1109
  exchange,
1065
- lambda text, _headers: _files.parse_link_completion(text),
1110
+ lambda text, _headers: _files.parse_link_completion(
1111
+ text,
1112
+ operation='upload link',
1113
+ entities=[
1114
+ {
1115
+ 'type': prepared.options.arguments.entity_type,
1116
+ 'id': prepared.options.arguments.entity_id,
1117
+ }
1118
+ ],
1119
+ ),
1066
1120
  )
1067
1121
 
1068
1122
  @staticmethod
@@ -1135,17 +1189,9 @@ class AsyncShotgun:
1135
1189
  prepared: _files.PreparedUpload,
1136
1190
  ) -> int:
1137
1191
  """Execute one prepared upload through a captured driver lease."""
1138
- arguments = prepared.options.arguments
1139
1192
  capabilities = await self._ensure_readiness(context, driver)
1140
1193
  operation_context = await self._authenticated_context(context, driver)
1141
- upload_path = _files.require_supported_upload_path(
1142
- _files.choose_upload_path(
1143
- capabilities.server_info,
1144
- arguments.entity_type,
1145
- arguments.field_name,
1146
- prepared.file_size,
1147
- )
1148
- )
1194
+ upload_path = _files.choose_prepared_upload_path(capabilities.server_info, prepared)
1149
1195
  multipart = upload_path == 'multipart'
1150
1196
  upload_info = await self._upload_allocation(
1151
1197
  driver,
@@ -1185,14 +1231,8 @@ class AsyncShotgun:
1185
1231
  ) -> int:
1186
1232
  """Execute the exact direct-storage thumbnail workflow."""
1187
1233
  capabilities = await self._ensure_readiness(context, driver)
1188
- arguments = prepared.options.arguments
1189
- _files.require_supported_upload_path(
1190
- _files.choose_upload_path(
1191
- capabilities.server_info,
1192
- arguments.entity_type,
1193
- arguments.field_name,
1194
- prepared.file_size,
1195
- )
1234
+ _files.choose_prepared_upload_path(
1235
+ capabilities.server_info, prepared, sharing_thumbnail=True
1196
1236
  )
1197
1237
  upload_info = await self._upload_allocation(
1198
1238
  driver,
@@ -1225,10 +1265,6 @@ class AsyncShotgun:
1225
1265
  result.update({field_name: current.get(field_name) for field_name in thumbnails})
1226
1266
  return result
1227
1267
 
1228
- @overload
1229
- async def activity_stream_read(self, entity_type: Never) -> dict[str, object]: ...
1230
-
1231
- @overload
1232
1268
  async def activity_stream_read(
1233
1269
  self,
1234
1270
  entity_type: str,
@@ -1237,34 +1273,30 @@ class AsyncShotgun:
1237
1273
  min_id: int | None = None,
1238
1274
  max_id: int | None = None,
1239
1275
  limit: int | None = None,
1240
- ) -> dict[str, object]: ...
1241
-
1242
- @_methods.expose_activity_stream_read_signature
1243
- async def activity_stream_read(
1244
- self,
1245
- *args: object,
1246
- **kwargs: object,
1247
1276
  ) -> dict[str, object]:
1248
1277
  """Read the activity stream for one entity.
1249
1278
 
1250
1279
  Args:
1251
- *args: Positional arguments from the documented activity-stream
1252
- signature.
1253
- **kwargs: Keyword arguments from the documented activity-stream
1254
- signature.
1280
+ entity_type: Entity type whose activity stream to read.
1281
+ entity_id: Numeric identifier of the entity.
1282
+ entity_fields: Optional fields to return for linked entity types.
1283
+ min_id: Optional minimum activity identifier.
1284
+ max_id: Optional maximum activity identifier.
1285
+ limit: Optional maximum number of activity records.
1255
1286
 
1256
1287
  Returns:
1257
1288
  Activity stream data returned by the server.
1258
1289
 
1259
1290
  Raises:
1260
- TypeError: If arguments do not match the documented signature.
1261
1291
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1262
1292
  or if the server result has the wrong type.
1263
1293
  ProtocolError: If a server request returns an HTTP error.
1264
1294
  ValueError: If authenticated HTTP violates the configured origin
1265
1295
  policy.
1266
1296
  """
1267
- options = _methods.bind_activity_stream_read(args, kwargs, 'AsyncShotgun')
1297
+ options = _methods.ActivityStreamReadOptions(
1298
+ entity_type, entity_id, entity_fields, min_id, max_id, limit
1299
+ )
1268
1300
  result = await self._call(
1269
1301
  'activity_stream',
1270
1302
  _methods.build_activity_stream_read_params(options),
@@ -1348,7 +1380,10 @@ class AsyncShotgun:
1348
1380
  type, or if the server result is not a list.
1349
1381
  ProtocolError: If a server request returns an HTTP error.
1350
1382
  ValueError: If authenticated HTTP violates the configured origin
1351
- policy.
1383
+ policy, or a naive datetime cannot be converted to UTC.
1384
+ OverflowError: If converting an aware datetime to UTC exceeds the
1385
+ supported datetime range.
1386
+ OSError: If the platform cannot resolve a local datetime.
1352
1387
  """
1353
1388
  calls = _crud.build_batch_calls(requests)
1354
1389
  if not calls:
@@ -1425,7 +1460,15 @@ class AsyncShotgun:
1425
1460
  )
1426
1461
 
1427
1462
  async def close(self) -> None:
1428
- """Swap out and drain the current transport without aborting work."""
1463
+ """Swap out and drain the current transport without aborting work.
1464
+
1465
+ Attempt cleanup of the remaining retired transports if one close fails,
1466
+ then propagate the first failure.
1467
+
1468
+ Raises:
1469
+ ShotgunError: If the built-in transport cannot finish cleanup.
1470
+ Exception: If a replacement transport's cleanup fails.
1471
+ """
1429
1472
  close_after_unlock: list[AsyncTransport] = []
1430
1473
  with self._transport_lock:
1431
1474
  advance_transport_generation(self._config)
@@ -1544,7 +1587,10 @@ class AsyncShotgun:
1544
1587
  file. A thumbnail failure occurs after the entity is created.
1545
1588
  ProtocolError: If a server request returns an HTTP error.
1546
1589
  ValueError: If authenticated HTTP violates the configured origin
1547
- policy.
1590
+ policy, or a naive datetime cannot be converted to UTC.
1591
+ OverflowError: If converting an aware datetime to UTC exceeds the
1592
+ supported datetime range.
1593
+ OSError: If the platform cannot resolve a local datetime.
1548
1594
  """
1549
1595
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=False)
1550
1596
  params = _crud.build_create_params(entity_type, field_data, return_fields)
@@ -1612,13 +1658,13 @@ class AsyncShotgun:
1612
1658
 
1613
1659
  Raises:
1614
1660
  TypeError: If no attachment is given or the reference is a string.
1615
- ValueError: If an entity dictionary references a different type.
1661
+ ValueError: If an entity dictionary references a different type,
1662
+ server settings are incomplete, or authenticated HTTP violates
1663
+ the configured origin security policy.
1616
1664
  ShotgunError: If another attachment reference is unsupported or a
1617
1665
  setup RPC reports a fault; the fault keeps its specific type.
1618
1666
  ShotgunFileDownloadError: If the resolved URL is invalid or setup,
1619
1667
  transfer, or destination I/O fails.
1620
- ValueError: If server settings are incomplete or authenticated HTTP
1621
- violates the configured origin security policy.
1622
1668
  """
1623
1669
  if attachment is False:
1624
1670
  if attachment_id is None:
@@ -1653,7 +1699,7 @@ class AsyncShotgun:
1653
1699
  headers=headers,
1654
1700
  url=url,
1655
1701
  )
1656
- return await exchange._execute(driver, operation)
1702
+ return await exchange.execute(driver, operation)
1657
1703
  except _errors.ShotgunFileDownloadError:
1658
1704
  raise
1659
1705
  except (TransportError, _errors.ProtocolError, OSError):
@@ -1668,57 +1714,39 @@ class AsyncShotgun:
1668
1714
  """
1669
1715
  raise _methods.entity_types_alias_error()
1670
1716
 
1671
- @overload
1672
1717
  async def export_page(
1673
1718
  self,
1674
1719
  page_id: int,
1675
- page_format: str,
1676
- /,
1720
+ format: str, # noqa: A002
1677
1721
  layout_name: str | None = None,
1678
- ) -> str: ...
1679
-
1680
- @overload
1681
- async def export_page(
1682
- self,
1683
- page_id: int,
1684
- **kwargs: Unpack[_methods.ExportPageKeywordArguments],
1685
- ) -> str: ...
1686
-
1687
- @_methods.expose_export_page_signature
1688
- async def export_page(self, *args: object, **kwargs: object) -> str:
1722
+ ) -> str:
1689
1723
  """Export a page using the documented arguments.
1690
1724
 
1691
1725
  The Page or its layout must be marked API Exportable in the web UI, or
1692
1726
  the server rejects the export.
1693
1727
 
1694
1728
  Args:
1695
- *args: Positional values from the documented `export_page`
1696
- signature.
1697
- **kwargs: Keyword values from the documented `export_page`
1698
- signature.
1729
+ page_id: Numeric page identifier.
1730
+ format: Required nonempty export format name.
1731
+ layout_name: Optional layout name, including an empty name.
1699
1732
 
1700
1733
  Returns:
1701
1734
  Exported page text returned by the server.
1702
1735
 
1703
1736
  Raises:
1704
- TypeError: If arguments do not match the documented signature.
1705
1737
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1706
- or if an argument or the server result has the wrong type.
1738
+ or if `page_id`, `format`, or the server result is invalid.
1707
1739
  ProtocolError: If a server request returns an HTTP error.
1708
1740
  ValueError: If authenticated HTTP violates the configured origin
1709
1741
  policy.
1710
1742
  """
1711
- options = _methods.bind_export_page(args, kwargs, 'AsyncShotgun')
1743
+ options = _methods.ExportPageOptions(page_id, format, layout_name)
1712
1744
  result = await self._call(
1713
1745
  'export_page',
1714
1746
  _methods.build_export_page_params(options),
1715
1747
  )
1716
1748
  return _methods.require_string_result(result, 'export_page')
1717
1749
 
1718
- @overload
1719
- async def find(self, entity_type: Never) -> list[dict[str, object]]: ...
1720
-
1721
- @overload
1722
1750
  async def find(
1723
1751
  self,
1724
1752
  entity_type: str,
@@ -1731,33 +1759,50 @@ class AsyncShotgun:
1731
1759
  page: int = 0,
1732
1760
  include_archived_projects: bool = True,
1733
1761
  additional_filter_presets: object = None,
1734
- ) -> list[dict[str, object]]: ...
1735
-
1736
- @_crud.expose_find_signature
1737
- async def find(self, *args: object, **kwargs: object) -> list[dict[str, object]]:
1762
+ ) -> list[dict[str, object]]:
1738
1763
  """Find entities matching the supplied filters.
1739
1764
 
1740
1765
  Args:
1741
- *args: Positional values from the documented `find` signature.
1742
- **kwargs: Keyword values from the documented `find` signature.
1766
+ entity_type: Entity type to find.
1767
+ filters: Filters in list or dict syntax.
1768
+ fields: Optional fields to return; defaults to `id`.
1769
+ order: Optional field and direction sort specifications.
1770
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
1771
+ limit: Maximum entities to return, or `0` for no limit.
1772
+ retired_only: Return retired entities instead of active entities.
1773
+ page: Specific page to return, or `0` to read all pages.
1774
+ include_archived_projects: Include entities from archived projects.
1775
+ additional_filter_presets: Optional server-side filter presets.
1743
1776
 
1744
1777
  Returns:
1745
1778
  Matching entities in server order.
1746
1779
 
1747
1780
  Raises:
1748
- TypeError: If arguments do not match the documented signature.
1749
1781
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1750
- or if filters, paging, ordering, or a server result is invalid.
1782
+ or if filters, fields, paging, ordering, or a server result is
1783
+ invalid.
1751
1784
  ProtocolError: If a server request returns an HTTP error.
1752
1785
  ValueError: If authenticated HTTP violates the configured origin
1753
- policy.
1786
+ policy, or a naive datetime cannot be converted to UTC.
1787
+ OverflowError: If converting an aware datetime to UTC exceeds the
1788
+ supported datetime range.
1789
+ OSError: If the platform cannot resolve a local datetime.
1754
1790
  """
1755
- return await self._find(_crud.bind_find(args, kwargs, 'AsyncShotgun'))
1756
-
1757
- @overload
1758
- async def find_one(self, entity_type: Never) -> dict[str, object] | None: ...
1791
+ return await self._find(
1792
+ _crud.FindOptions(
1793
+ entity_type,
1794
+ filters,
1795
+ fields,
1796
+ order,
1797
+ filter_operator,
1798
+ limit,
1799
+ retired_only,
1800
+ page,
1801
+ include_archived_projects,
1802
+ additional_filter_presets,
1803
+ )
1804
+ )
1759
1805
 
1760
- @overload
1761
1806
  async def find_one(
1762
1807
  self,
1763
1808
  entity_type: str,
@@ -1768,28 +1813,43 @@ class AsyncShotgun:
1768
1813
  retired_only: bool = False,
1769
1814
  include_archived_projects: bool = True,
1770
1815
  additional_filter_presets: object = None,
1771
- ) -> dict[str, object] | None: ...
1772
-
1773
- @_crud.expose_find_one_signature
1774
- async def find_one(self, *args: object, **kwargs: object) -> dict[str, object] | None:
1816
+ ) -> dict[str, object] | None:
1775
1817
  """Return the first entity matching the supplied filters.
1776
1818
 
1777
1819
  Args:
1778
- *args: Positional values from the documented `find_one` signature.
1779
- **kwargs: Keyword values from the documented `find_one` signature.
1820
+ entity_type: Entity type to find.
1821
+ filters: Filters in list or dict syntax.
1822
+ fields: Optional fields to return; defaults to `id`.
1823
+ order: Optional field and direction sort specifications.
1824
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
1825
+ retired_only: Return a retired entity instead of an active entity.
1826
+ include_archived_projects: Include entities from archived projects.
1827
+ additional_filter_presets: Optional server-side filter presets.
1780
1828
 
1781
1829
  Returns:
1782
1830
  The first matching entity, or `None` when no entity matches.
1783
1831
 
1784
1832
  Raises:
1785
- TypeError: If arguments do not match the documented signature.
1786
1833
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1787
- or if filters, paging, ordering, or a server result is invalid.
1834
+ or if filters, fields, paging, ordering, or a server result is
1835
+ invalid.
1788
1836
  ProtocolError: If a server request returns an HTTP error.
1789
1837
  ValueError: If authenticated HTTP violates the configured origin
1790
- policy.
1838
+ policy, or a naive datetime cannot be converted to UTC.
1839
+ OverflowError: If converting an aware datetime to UTC exceeds the
1840
+ supported datetime range.
1841
+ OSError: If the platform cannot resolve a local datetime.
1791
1842
  """
1792
- options = _crud.bind_find_one(args, kwargs, 'AsyncShotgun')
1843
+ options = _crud.FindOneOptions(
1844
+ entity_type,
1845
+ filters,
1846
+ fields,
1847
+ order,
1848
+ filter_operator,
1849
+ retired_only,
1850
+ include_archived_projects,
1851
+ additional_filter_presets,
1852
+ )
1793
1853
  return _crud.first_result(await self._find(_crud.find_options_from_one(options)))
1794
1854
 
1795
1855
  async def follow(
@@ -2186,7 +2246,10 @@ class AsyncShotgun:
2186
2246
  or if the server result is invalid.
2187
2247
  ProtocolError: If a server request returns an HTTP error.
2188
2248
  ValueError: If authenticated HTTP violates the configured origin
2189
- policy.
2249
+ policy, or a naive datetime cannot be converted to UTC.
2250
+ OverflowError: If converting an aware datetime to UTC exceeds the
2251
+ supported datetime range.
2252
+ OSError: If the platform cannot resolve a local datetime.
2190
2253
  """
2191
2254
  params = _methods.build_schema_field_create_params(
2192
2255
  entity_type,
@@ -2274,7 +2337,10 @@ class AsyncShotgun:
2274
2337
  or if the server result is invalid.
2275
2338
  ProtocolError: If a server request returns an HTTP error.
2276
2339
  ValueError: If authenticated HTTP violates the configured origin
2277
- policy.
2340
+ policy, or a naive datetime cannot be converted to UTC.
2341
+ OverflowError: If converting an aware datetime to UTC exceeds the
2342
+ supported datetime range.
2343
+ OSError: If the platform cannot resolve a local datetime.
2278
2344
  """
2279
2345
  params = _methods.build_schema_field_update_params(
2280
2346
  entity_type,
@@ -2389,30 +2455,26 @@ class AsyncShotgun:
2389
2455
  source_entity,
2390
2456
  filmstrip_thumbnail,
2391
2457
  )
2392
- if share.thumbnail_path is not None and 'field_name' in kwargs:
2393
- raise TypeError("AsyncShotgun.upload() got multiple values for argument 'field_name'")
2394
2458
  prepared: _files.PreparedUpload | None = None
2395
2459
  if share.thumbnail_path is not None:
2396
- arguments = _bind_upload(
2397
- (
2398
- share.source_entity_type,
2399
- share.source_entity_id,
2400
- share.thumbnail_path,
2401
- ),
2402
- {
2403
- **kwargs,
2404
- 'field_name': (
2405
- 'filmstrip_thumb_image' if filmstrip_thumbnail else 'thumb_image'
2406
- ),
2407
- },
2460
+ for keyword in kwargs:
2461
+ if keyword not in ('display_name', 'tag_list', 'created_at'):
2462
+ raise TypeError(
2463
+ 'AsyncShotgun.share_thumbnail() got an unexpected keyword '
2464
+ f"argument '{keyword}'"
2465
+ )
2466
+ arguments = _files.UploadArguments(
2467
+ entity_type=share.source_entity_type,
2468
+ entity_id=share.source_entity_id,
2469
+ path=share.thumbnail_path,
2470
+ field_name=_files.thumbnail_fields(filmstrip_thumbnail)[0],
2471
+ display_name=cast('str | None', kwargs.get('display_name')),
2472
+ tag_list=cast('str | None', kwargs.get('tag_list')),
2473
+ created_at=cast('_dt_module.datetime | None', kwargs.get('created_at')),
2408
2474
  )
2409
2475
  prepared = await asyncio.to_thread(
2410
2476
  _files.prepare_upload,
2411
- _files.UploadOptions(
2412
- arguments=arguments,
2413
- upload_type='Thumbnail',
2414
- filmstrip=filmstrip_thumbnail,
2415
- ),
2477
+ _files.upload_options(arguments),
2416
2478
  )
2417
2479
  context = config_module.snapshot(self._config)
2418
2480
  require_secure_rpc(context, include_auth=True)
@@ -2427,9 +2489,7 @@ class AsyncShotgun:
2427
2489
  )
2428
2490
  if len(share.entities) == 1:
2429
2491
  return attachment_id
2430
- field_name: Literal['filmstrip_image', 'image'] = (
2431
- 'filmstrip_image' if filmstrip_thumbnail else 'image'
2432
- )
2492
+ field_name = _files.thumbnail_fields(filmstrip_thumbnail)[1]
2433
2493
  ready = await driver.poll(
2434
2494
  lambda: self._thumbnail_source_ready(
2435
2495
  driver,
@@ -2458,10 +2518,6 @@ class AsyncShotgun:
2458
2518
  filmstrip_thumbnail,
2459
2519
  )
2460
2520
 
2461
- @overload
2462
- async def summarize(self, entity_type: Never) -> dict[str, object]: ...
2463
-
2464
- @overload
2465
2521
  async def summarize(
2466
2522
  self,
2467
2523
  entity_type: str,
@@ -2470,28 +2526,39 @@ class AsyncShotgun:
2470
2526
  filter_operator: str | None = None,
2471
2527
  grouping: list[dict[str, Any]] | None = None,
2472
2528
  include_archived_projects: bool = True,
2473
- ) -> dict[str, object]: ...
2474
-
2475
- @_methods.expose_summarize_signature
2476
- async def summarize(self, *args: object, **kwargs: object) -> dict[str, object]:
2529
+ ) -> dict[str, object]:
2477
2530
  """Summarize field data returned by a query.
2478
2531
 
2479
2532
  Args:
2480
- *args: Positional values from the documented `summarize` signature.
2481
- **kwargs: Keyword values from the documented `summarize` signature.
2533
+ entity_type: Entity type to summarize.
2534
+ filters: Filters in list or dict syntax.
2535
+ summary_fields: List of `{'field': ..., 'type': ...}` summaries.
2536
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
2537
+ grouping: Optional grouping specs.
2538
+ include_archived_projects: Include archived projects when `True`.
2482
2539
 
2483
2540
  Returns:
2484
- Dict with `summaries` and optional `groups`.
2541
+ Dict with `summaries` and (when grouped) `groups`.
2485
2542
 
2486
2543
  Raises:
2487
- TypeError: If arguments do not match the documented signature.
2488
2544
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2489
- or if arguments or the server result are invalid.
2545
+ or if `filters`, `summary_fields`, or the server result has the
2546
+ wrong type.
2490
2547
  ProtocolError: If a server request returns an HTTP error.
2491
2548
  ValueError: If authenticated HTTP violates the configured origin
2492
- policy.
2549
+ policy, or a naive datetime cannot be converted to UTC.
2550
+ OverflowError: If converting an aware datetime to UTC exceeds the
2551
+ supported datetime range.
2552
+ OSError: If the platform cannot resolve a local datetime.
2493
2553
  """
2494
- options = _methods.bind_summarize(args, kwargs, 'AsyncShotgun')
2554
+ options = _methods.SummarizeOptions(
2555
+ entity_type,
2556
+ filters,
2557
+ summary_fields,
2558
+ filter_operator,
2559
+ grouping,
2560
+ include_archived_projects,
2561
+ )
2495
2562
  params = _methods.build_summarize_params(options)
2496
2563
  result = await self._call('summarize', params)
2497
2564
  return _crud.require_dictionary_result(result, 'summarize')
@@ -2519,7 +2586,10 @@ class AsyncShotgun:
2519
2586
  or if arguments or the server result are invalid.
2520
2587
  ProtocolError: If a server request returns an HTTP error.
2521
2588
  ValueError: If authenticated HTTP violates the configured origin
2522
- policy.
2589
+ policy, or a naive datetime cannot be converted to UTC.
2590
+ OverflowError: If converting an aware datetime to UTC exceeds the
2591
+ supported datetime range.
2592
+ OSError: If the platform cannot resolve a local datetime.
2523
2593
  """
2524
2594
  params = _methods.build_text_search_params(
2525
2595
  text,
@@ -2589,7 +2659,10 @@ class AsyncShotgun:
2589
2659
  nonempty file. A thumbnail failure occurs after the update.
2590
2660
  ProtocolError: If a server request returns an HTTP error.
2591
2661
  ValueError: If authenticated HTTP violates the configured origin
2592
- policy.
2662
+ policy, or a naive datetime cannot be converted to UTC.
2663
+ OverflowError: If converting an aware datetime to UTC exceeds the
2664
+ supported datetime range.
2665
+ OSError: If the platform cannot resolve a local datetime.
2593
2666
  """
2594
2667
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=True)
2595
2668
  params = _crud.build_update_params(
@@ -2620,7 +2693,8 @@ class AsyncShotgun:
2620
2693
  user: Optional user entity to attribute the access to.
2621
2694
 
2622
2695
  Raises:
2623
- ShotgunError: If an RPC fault or terminal transport failure occurs.
2696
+ ShotgunError: If a project or user ID is invalid, or an RPC fault
2697
+ or terminal transport failure occurs.
2624
2698
  ProtocolError: If a server request returns an HTTP error.
2625
2699
  ValueError: If authenticated HTTP violates the configured origin
2626
2700
  policy.
@@ -2631,10 +2705,6 @@ class AsyncShotgun:
2631
2705
  _InvocationOptions(logical_method_name='update_project_last_accessed'),
2632
2706
  )
2633
2707
 
2634
- @overload
2635
- async def upload(self, entity_type: Never) -> int: ...
2636
-
2637
- @overload
2638
2708
  async def upload(
2639
2709
  self,
2640
2710
  entity_type: str,
@@ -2644,24 +2714,26 @@ class AsyncShotgun:
2644
2714
  display_name: str | None = None,
2645
2715
  tag_list: str | None = None,
2646
2716
  created_at: _dt_module.datetime | None = None,
2647
- ) -> int: ...
2648
-
2649
- @_expose_upload_signature
2650
- async def upload(self, *args: object, **kwargs: object) -> int:
2717
+ ) -> int:
2651
2718
  """Upload a file or thumbnail and return its Attachment ID.
2652
2719
 
2653
2720
  A server requiring legacy form uploads raises `ShotgunError` before any
2654
2721
  upload request is sent. Server capability reads may occur.
2655
2722
 
2656
2723
  Args:
2657
- *args: Positional arguments from the documented upload signature.
2658
- **kwargs: Keyword arguments from the documented upload signature.
2724
+ entity_type: Entity type receiving the upload.
2725
+ entity_id: Numeric identifier of the receiving entity.
2726
+ path: Existing non-empty file path as text or filesystem-encoded
2727
+ bytes.
2728
+ field_name: Optional file field receiving an Attachment.
2729
+ display_name: Optional Attachment display name.
2730
+ tag_list: Optional comma-delimited Attachment tags.
2731
+ created_at: Optional Attachment creation timestamp.
2659
2732
 
2660
2733
  Returns:
2661
2734
  Created Attachment ID.
2662
2735
 
2663
2736
  Raises:
2664
- TypeError: If arguments do not match the documented signature.
2665
2737
  ShotgunError: If the file or an upload response is invalid, an RPC
2666
2738
  fault or terminal transport failure occurs, or the server
2667
2739
  requires legacy form uploads.
@@ -2669,23 +2741,10 @@ class AsyncShotgun:
2669
2741
  ValueError: If authenticated HTTP violates the configured origin
2670
2742
  policy.
2671
2743
  """
2672
- arguments = _bind_upload(args, kwargs)
2673
- thumbnail_fields = {
2674
- 'filmstrip_image',
2675
- 'filmstrip_thumb_image',
2676
- 'image',
2677
- 'thumb_image',
2678
- }
2679
- upload_type: Literal['Attachment', 'Thumbnail'] = (
2680
- 'Thumbnail' if arguments.field_name in thumbnail_fields else 'Attachment'
2681
- )
2682
- return await self._upload(
2683
- _files.UploadOptions(
2684
- arguments=arguments,
2685
- upload_type=upload_type,
2686
- filmstrip=arguments.field_name in {'filmstrip_image', 'filmstrip_thumb_image'},
2687
- )
2744
+ arguments = _files.UploadArguments(
2745
+ entity_type, entity_id, path, field_name, display_name, tag_list, created_at
2688
2746
  )
2747
+ return await self._upload(_files.upload_options(arguments))
2689
2748
 
2690
2749
  async def upload_filmstrip_thumbnail(
2691
2750
  self,
@@ -2845,10 +2904,6 @@ class AsyncShotgun:
2845
2904
  result = await self._call('work_schedule_read', params)
2846
2905
  return _crud.require_dictionary_result(result, 'work_schedule_read')
2847
2906
 
2848
- @overload
2849
- async def work_schedule_update(self, date: Never) -> dict[str, object]: ...
2850
-
2851
- @overload
2852
2907
  async def work_schedule_update(
2853
2908
  self,
2854
2909
  date: _dt_module.date | str,
@@ -2857,37 +2912,49 @@ class AsyncShotgun:
2857
2912
  project: dict[str, Any] | None = None,
2858
2913
  user: dict[str, Any] | None = None,
2859
2914
  recalculate_field: str | None = None,
2860
- ) -> dict[str, object]: ...
2861
-
2862
- @_methods.expose_work_schedule_update_signature
2863
- async def work_schedule_update(
2864
- self,
2865
- *args: object,
2866
- **kwargs: object,
2867
2915
  ) -> dict[str, object]:
2868
- """Update a work schedule using the documented arguments.
2916
+ """Update a work schedule.
2869
2917
 
2870
2918
  Args:
2871
- *args: Positional values from the documented public signature.
2872
- **kwargs: Keyword values from the documented public signature.
2919
+ date: Date or exact `YYYY-MM-DD` string to update.
2920
+ working: Whether the date is a working day.
2921
+ description: Optional schedule description.
2922
+ project: Optional project entity restriction.
2923
+ user: Optional user entity restriction.
2924
+ recalculate_field: Optional field to recalculate.
2873
2925
 
2874
2926
  Returns:
2875
2927
  Updated work schedule data returned by the server.
2876
2928
 
2877
2929
  Raises:
2878
- TypeError: If arguments do not match the documented signature.
2879
2930
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2880
- or if arguments or the server result are invalid.
2931
+ or if the date, project/user exclusivity, or server result is
2932
+ invalid.
2881
2933
  ProtocolError: If a server request returns an HTTP error.
2882
2934
  ValueError: If authenticated HTTP violates the configured origin
2883
2935
  policy.
2884
2936
  """
2885
- options = _methods.bind_work_schedule_update(args, kwargs, 'AsyncShotgun')
2937
+ options = _methods.WorkScheduleUpdateOptions(
2938
+ date, working, description, project, user, recalculate_field
2939
+ )
2886
2940
  params = _methods.build_work_schedule_update_params(options)
2887
2941
  result = await self._call('work_schedule_update', params)
2888
2942
  return _crud.require_dictionary_result(result, 'work_schedule_update')
2889
2943
 
2890
2944
 
2945
+ async def _capture_download_cleanup(
2946
+ workers: _AsyncDownloadCancellationScope,
2947
+ function: Callable[_FilesystemParams, _FilesystemResult],
2948
+ *args: _FilesystemParams.args,
2949
+ **kwargs: _FilesystemParams.kwargs,
2950
+ ) -> BaseException | None:
2951
+ """Run one joined cleanup operation and capture its failure."""
2952
+ capture = _AsyncDownloadCleanupCapture(workers)
2953
+ with capture:
2954
+ await workers.run(function, *args, **kwargs)
2955
+ return capture.exception
2956
+
2957
+
2891
2958
  def _open_download_temporary(destination: Path) -> tuple[ExitStack, BinaryIO, Path]:
2892
2959
  """Open one adjacent download temporary file."""
2893
2960
  stack = ExitStack()
@@ -2920,7 +2987,6 @@ async def _execute_upload_exchange(
2920
2987
  headers=dict(exchange.headers),
2921
2988
  body=exchange.body_factory(),
2922
2989
  timeout=context.timeout,
2923
- token=uuid.uuid4().hex,
2924
2990
  )
2925
2991
 
2926
2992
  async def consume(