sg-python-api 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {sg_python_api-0.1.1.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
  2. sg_python_api-0.2.0.dist-info/RECORD +58 -0
  3. shotgun_api3/__init__.py +4 -0
  4. shotgun_api3/_async_client.py +404 -316
  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 +354 -462
  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 +215 -123
  16. shotgun_api3/_methods.py +153 -412
  17. shotgun_api3/_protocol.py +27 -146
  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 +1031 -0
  26. shotgun_api3/lib/mockgun/_follows.py +217 -0
  27. shotgun_api3/lib/mockgun/_handlers.py +558 -0
  28. shotgun_api3/lib/mockgun/_links.py +440 -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 +2163 -0
  32. shotgun_api3/lib/mockgun/_retirement.py +368 -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 +1446 -0
  42. shotgun_api3/lib/mockgun/_summaries.py +964 -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.1.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.1.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
  56. {sg_python_api-0.1.1.dist-info → sg_python_api-0.2.0.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,
@@ -59,9 +51,7 @@ from ._config import (
59
51
  server_host,
60
52
  server_page_limit,
61
53
  )
62
- from ._errors import ShotgunError
63
54
  from ._execution import SuppressedExceptionCapture, release_lease, retire_transport
64
- from ._signatures import bind_arguments, expose_signature
65
55
  from ._transport import (
66
56
  ReadTimeoutError,
67
57
  SendError,
@@ -81,11 +71,6 @@ _FilesystemParams = ParamSpec('_FilesystemParams')
81
71
  _FilesystemResult = TypeVar('_FilesystemResult')
82
72
  _UploadResult = TypeVar('_UploadResult')
83
73
 
84
- _METHOD_UPLOAD_SIGNATURE = inspect.signature(Shotgun.upload)
85
- _UPLOAD_SIGNATURE = inspect.signature(Shotgun.upload).replace(
86
- parameters=tuple(inspect.signature(Shotgun.upload).parameters.values())[1:]
87
- )
88
-
89
74
 
90
75
  class _AsyncDownloadCancellationScope:
91
76
  """Own and join every filesystem worker started by one transfer."""
@@ -95,12 +80,17 @@ class _AsyncDownloadCancellationScope:
95
80
  self._captured_cancellation: asyncio.CancelledError | None = None
96
81
 
97
82
  @property
98
- def _cancellation(self) -> asyncio.CancelledError | None:
83
+ def cancellation(self) -> asyncio.CancelledError | None:
99
84
  """Return the first cancellation captured while joining a worker."""
100
85
  return self._captured_cancellation
101
86
 
102
- def _capture_cancellation(self, error: BaseException) -> None:
103
- """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
+ """
104
94
  current = asyncio.current_task()
105
95
  if (
106
96
  not isinstance(error, asyncio.CancelledError)
@@ -112,28 +102,45 @@ class _AsyncDownloadCancellationScope:
112
102
  self._captured_cancellation = error
113
103
  current.uncancel()
114
104
 
115
- def _raise_if_cancelled(self) -> None:
116
- """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
+ """
117
111
  if self._captured_cancellation is not None:
118
112
  raise self._captured_cancellation
119
113
 
120
- async def _run(
114
+ async def run(
121
115
  self,
122
116
  function: Callable[_FilesystemParams, _FilesystemResult],
123
117
  *args: _FilesystemParams.args,
124
118
  **kwargs: _FilesystemParams.kwargs,
125
119
  ) -> _FilesystemResult:
126
- """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
+ """
127
134
  task = asyncio.create_task(asyncio.to_thread(function, *args, **kwargs))
128
135
  while not task.done():
129
136
  try:
130
137
  await asyncio.wait({task})
131
138
  except asyncio.CancelledError as error:
132
- self._capture_cancellation(error)
139
+ self.capture_cancellation(error)
133
140
  try:
134
141
  return task.result()
135
142
  except BaseException:
136
- self._raise_if_cancelled()
143
+ self.raise_if_cancelled()
137
144
  raise
138
145
 
139
146
 
@@ -156,7 +163,7 @@ class _AsyncDownloadCleanupCapture:
156
163
  _traceback: TracebackType | None,
157
164
  ) -> bool:
158
165
  """Capture and suppress one async download-cleanup exception."""
159
- self.exception = self._workers._cancellation or exception
166
+ self.exception = self._workers.cancellation or exception
160
167
  return exception is not None
161
168
 
162
169
 
@@ -194,35 +201,38 @@ class _AsyncDownloadExchange:
194
201
  workers = _AsyncDownloadCancellationScope()
195
202
  try:
196
203
  try:
197
- stack, handle, temporary = await workers._run(
198
- _open_download_temporary,
199
- destination,
200
- )
201
- 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()
202
210
  opened_handle = handle
203
211
 
204
212
  async def write(chunk: bytes) -> None:
205
- await workers._run(opened_handle.write, chunk)
206
- workers._raise_if_cancelled()
213
+ with _files.download_destination_io(destination):
214
+ await workers.run(opened_handle.write, chunk)
215
+ workers.raise_if_cancelled()
207
216
 
208
217
  await control.consume_into(chunks, write, total_bytes=total_bytes)
209
- await workers._run(handle.flush)
210
- workers._raise_if_cancelled()
218
+ with _files.download_destination_io(destination):
219
+ await workers.run(handle.flush)
220
+ workers.raise_if_cancelled()
211
221
  close_exception = await _capture_download_cleanup(workers, stack.close)
212
- stack = None
213
- handle = None
214
- workers._raise_if_cancelled()
222
+ stack, handle = None, None
223
+ workers.raise_if_cancelled()
215
224
  if close_exception is not None:
216
225
  _raise_download_cleanup_error()
217
226
  # This is the final cancellation checkpoint before atomic commit.
218
227
  # Once `replace` starts, its worker is joined before cancellation
219
228
  # propagates, so the destination cannot change after return.
220
229
  await asyncio.sleep(0)
221
- await workers._run(temporary.replace, destination)
222
- workers._raise_if_cancelled()
230
+ with _files.download_destination_io(destination):
231
+ await workers.run(temporary.replace, destination)
232
+ workers.raise_if_cancelled()
223
233
  except BaseException as error:
224
234
  operation_exception = error
225
- workers._capture_cancellation(error)
235
+ workers.capture_cancellation(error)
226
236
  raise
227
237
  finally:
228
238
  close_exception: BaseException | None = None
@@ -235,7 +245,7 @@ class _AsyncDownloadExchange:
235
245
  temporary.unlink,
236
246
  missing_ok=True,
237
247
  )
238
- workers._raise_if_cancelled()
248
+ workers.raise_if_cancelled()
239
249
  if operation_exception is None:
240
250
  if close_exception is not None:
241
251
  raise _AsyncDownloadCleanupError from None
@@ -259,12 +269,23 @@ class _AsyncDownloadExchange:
259
269
  chunks = streaming.aiter_chunks(_ASYNC_FILE_BATCH_SIZE)
260
270
  return await self._write_file(chunks, control, total_bytes)
261
271
 
262
- async def _execute(
272
+ async def execute(
263
273
  self,
264
274
  driver: AsyncDriver,
265
275
  operation: _protocol.OperationDescriptor,
266
276
  ) -> bytes | str:
267
- """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
+ """
268
289
  try:
269
290
  return await driver.execute(
270
291
  operation,
@@ -311,36 +332,6 @@ class _TokenExchangeProgress:
311
332
  token: str | None = None
312
333
 
313
334
 
314
- def _bind_upload(
315
- args: tuple[object, ...],
316
- kwargs: dict[str, object],
317
- ) -> _files.UploadArguments:
318
- """Bind runtime arguments with upload-specific errors."""
319
- bound = bind_arguments(_UPLOAD_SIGNATURE, args, kwargs, 'AsyncShotgun.upload()')
320
- arguments = cast('Callable[..., _files.UploadArguments]', _files.UploadArguments)
321
- return arguments(**bound.arguments)
322
-
323
-
324
- async def _capture_download_cleanup(
325
- workers: _AsyncDownloadCancellationScope,
326
- function: Callable[_FilesystemParams, _FilesystemResult],
327
- *args: _FilesystemParams.args,
328
- **kwargs: _FilesystemParams.kwargs,
329
- ) -> BaseException | None:
330
- """Run one joined cleanup operation and capture its failure."""
331
- capture = _AsyncDownloadCleanupCapture(workers)
332
- with capture:
333
- await workers._run(function, *args, **kwargs)
334
- return capture.exception
335
-
336
-
337
- def _expose_upload_signature(function: Callable[..., object]) -> Callable[..., object]:
338
- """Attach the implemented sync upload signature and annotations."""
339
- expose_signature(function, _METHOD_UPLOAD_SIGNATURE, Shotgun.upload.__doc__)
340
- object.__setattr__(function, '__annotations__', Shotgun.upload.__annotations__)
341
- return function
342
-
343
-
344
335
  class AsyncShotgun:
345
336
  """Provide the asynchronous Flow Production Tracking API client surface.
346
337
 
@@ -357,12 +348,8 @@ class AsyncShotgun:
357
348
  server_info: Cached raw metadata for the configured origin.
358
349
  """
359
350
 
360
- __signature__ = CONSTRUCTOR_SIGNATURE
351
+ _in_process: ClassVar[bool] = False
361
352
 
362
- @overload
363
- def __init__(self, base_url: Never) -> None: ...
364
-
365
- @overload
366
353
  def __init__(
367
354
  self,
368
355
  base_url: str,
@@ -377,10 +364,7 @@ class AsyncShotgun:
377
364
  sudo_as_login: str | None = None,
378
365
  session_token: str | None = None,
379
366
  auth_token: str | None = None,
380
- ) -> None: ...
381
-
382
- @expose_constructor_signature
383
- def __init__(self, *args: object, **kwargs: object) -> None:
367
+ ) -> None:
384
368
  """Initialize a client for one site and authentication mode.
385
369
 
386
370
  Supply exactly one authentication mode: `script_name` with `api_key`,
@@ -392,6 +376,8 @@ class AsyncShotgun:
392
376
  `http_proxy` accepts `[user:pass@]host[:port]`, with port 8080 when
393
377
  omitted. A truthy `ca_certs` selects a CA bundle; otherwise
394
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.
395
381
 
396
382
  Constructor validation is synchronous; readiness and authentication
397
383
  exchange I/O begins only through an operation, `connect()`, or an async
@@ -399,20 +385,57 @@ class AsyncShotgun:
399
385
  I/O.
400
386
 
401
387
  Args:
402
- *args: Positional values from the documented constructor signature.
403
- **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.
404
403
 
405
404
  Raises:
406
- TypeError: If arguments do not match the documented signature.
407
- 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.
408
408
  """
409
- 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
+ )
410
425
 
411
426
  async def __aenter__(self) -> Self:
412
427
  """Enter this client and perform any configured eager connection.
413
428
 
414
429
  Returns:
415
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.
416
439
  """
417
440
  if not self._connect_on_enter:
418
441
  return self
@@ -428,6 +451,10 @@ class AsyncShotgun:
428
451
 
429
452
  Args:
430
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.
431
458
  """
432
459
  await self.close()
433
460
 
@@ -483,10 +510,14 @@ class AsyncShotgun:
483
510
  def _initialize(self, options: ConstructorOptions) -> None:
484
511
  """Initialize network-free state from validated constructor options."""
485
512
  self._base_url = options.base_url.rstrip('/')
486
- 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
+ )
487
518
  self._client_caps = ClientCapabilities()
488
519
  self._connect_on_enter = options.connect
489
- self._config = build_config(options)
520
+ self._config = build_config(options, in_process=self._in_process)
490
521
 
491
522
  self._driver: AsyncDriver | None = None
492
523
  self._driver_generation: int | None = None
@@ -547,7 +578,11 @@ class AsyncShotgun:
547
578
  raise cancellation
548
579
 
549
580
  def _new_transport(self, context: RequestContext) -> AsyncTransport:
550
- """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')
551
586
  module = importlib.import_module(f'{__package__}._async_httpx2')
552
587
  factory = cast(
553
588
  'type[Httpx2Transport]',
@@ -558,6 +593,8 @@ class AsyncShotgun:
558
593
  proxy_headers=dict(context.proxy_headers) or None,
559
594
  proxy_url=context.proxy_url,
560
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,
561
598
  )
562
599
 
563
600
  def _acquire_driver(
@@ -567,7 +604,14 @@ class AsyncShotgun:
567
604
  """Lease one generation's driver and return idle transports to close."""
568
605
  close_after_unlock: list[AsyncTransport] = []
569
606
  with self._transport_lock:
570
- 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:
571
615
  if self._known_transport is not None:
572
616
  retired = retire_transport(
573
617
  self._lease_counts, self._retired_transports, self._known_transport
@@ -585,16 +629,14 @@ class AsyncShotgun:
585
629
  self._transport = transport
586
630
  self._known_transport = transport
587
631
  self._transport_owned = True
588
- elif self._driver_generation not in {None, context.transport_generation}:
589
- if self._transport_owned:
590
- retired = retire_transport(
591
- self._lease_counts, self._retired_transports, transport
592
- )
593
- if retired is not None:
594
- close_after_unlock.append(retired)
595
- transport = self._new_transport(context)
596
- self._transport = transport
597
- 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:
598
640
  self._driver = None
599
641
 
600
642
  if self._driver is None:
@@ -952,7 +994,9 @@ class AsyncShotgun:
952
994
  driver,
953
995
  context,
954
996
  exchange,
955
- lambda text, _headers: _files.parse_link_completion(text),
997
+ lambda text, _headers: _files.parse_link_completion(
998
+ text, operation='share_thumbnail', entities=entities
999
+ ),
956
1000
  )
957
1001
 
958
1002
  async def _thumbnail_source_ready(
@@ -1026,7 +1070,7 @@ class AsyncShotgun:
1026
1070
  _files.StorageRange(
1027
1071
  length=prepared.file_size,
1028
1072
  offset=0,
1029
- path=prepared.options.arguments.path,
1073
+ path=prepared.path,
1030
1074
  ),
1031
1075
  chunk_size=_ASYNC_FILE_BATCH_SIZE,
1032
1076
  )
@@ -1037,29 +1081,6 @@ class AsyncShotgun:
1037
1081
  lambda _text, _headers: None,
1038
1082
  )
1039
1083
 
1040
- @staticmethod
1041
- async def _upload_form(
1042
- driver: AsyncDriver,
1043
- context: RequestContext,
1044
- prepared: _files.PreparedUpload,
1045
- ) -> Never:
1046
- """Send the observed form request without guessing a success result."""
1047
- if context.canonical_origin is None:
1048
- raise ValueError('server parameters must be configured before upload I/O')
1049
- exchange = _files.build_form_upload_exchange(
1050
- context.canonical_origin,
1051
- context.user_agent,
1052
- prepared,
1053
- context.auth,
1054
- chunk_size=_ASYNC_FILE_BATCH_SIZE,
1055
- )
1056
-
1057
- def reject_success(_text: str, _headers: Mapping[str, str]) -> Never:
1058
- raise ShotgunError('form upload success response is not established')
1059
-
1060
- await _execute_upload_exchange(driver, context, exchange, reject_success)
1061
- raise AssertionError('form upload response parser returned')
1062
-
1063
1084
  @staticmethod
1064
1085
  async def _upload_link(
1065
1086
  driver: AsyncDriver,
@@ -1086,7 +1107,16 @@ class AsyncShotgun:
1086
1107
  driver,
1087
1108
  context,
1088
1109
  exchange,
1089
- 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
+ ),
1090
1120
  )
1091
1121
 
1092
1122
  @staticmethod
@@ -1159,17 +1189,9 @@ class AsyncShotgun:
1159
1189
  prepared: _files.PreparedUpload,
1160
1190
  ) -> int:
1161
1191
  """Execute one prepared upload through a captured driver lease."""
1162
- arguments = prepared.options.arguments
1163
1192
  capabilities = await self._ensure_readiness(context, driver)
1164
1193
  operation_context = await self._authenticated_context(context, driver)
1165
- upload_path = _files.choose_upload_path(
1166
- capabilities.server_info,
1167
- arguments.entity_type,
1168
- arguments.field_name,
1169
- prepared.file_size,
1170
- )
1171
- if upload_path == 'form':
1172
- return await self._upload_form(driver, operation_context, prepared)
1194
+ upload_path = _files.choose_prepared_upload_path(capabilities.server_info, prepared)
1173
1195
  multipart = upload_path == 'multipart'
1174
1196
  upload_info = await self._upload_allocation(
1175
1197
  driver,
@@ -1208,6 +1230,10 @@ class AsyncShotgun:
1208
1230
  prepared: _files.PreparedUpload,
1209
1231
  ) -> int:
1210
1232
  """Execute the exact direct-storage thumbnail workflow."""
1233
+ capabilities = await self._ensure_readiness(context, driver)
1234
+ _files.choose_prepared_upload_path(
1235
+ capabilities.server_info, prepared, sharing_thumbnail=True
1236
+ )
1211
1237
  upload_info = await self._upload_allocation(
1212
1238
  driver,
1213
1239
  context,
@@ -1239,10 +1265,6 @@ class AsyncShotgun:
1239
1265
  result.update({field_name: current.get(field_name) for field_name in thumbnails})
1240
1266
  return result
1241
1267
 
1242
- @overload
1243
- async def activity_stream_read(self, entity_type: Never) -> dict[str, object]: ...
1244
-
1245
- @overload
1246
1268
  async def activity_stream_read(
1247
1269
  self,
1248
1270
  entity_type: str,
@@ -1251,34 +1273,30 @@ class AsyncShotgun:
1251
1273
  min_id: int | None = None,
1252
1274
  max_id: int | None = None,
1253
1275
  limit: int | None = None,
1254
- ) -> dict[str, object]: ...
1255
-
1256
- @_methods.expose_activity_stream_read_signature
1257
- async def activity_stream_read(
1258
- self,
1259
- *args: object,
1260
- **kwargs: object,
1261
1276
  ) -> dict[str, object]:
1262
1277
  """Read the activity stream for one entity.
1263
1278
 
1264
1279
  Args:
1265
- *args: Positional arguments from the documented activity-stream
1266
- signature.
1267
- **kwargs: Keyword arguments from the documented activity-stream
1268
- 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.
1269
1286
 
1270
1287
  Returns:
1271
1288
  Activity stream data returned by the server.
1272
1289
 
1273
1290
  Raises:
1274
- TypeError: If arguments do not match the documented signature.
1275
1291
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1276
1292
  or if the server result has the wrong type.
1277
1293
  ProtocolError: If a server request returns an HTTP error.
1278
1294
  ValueError: If authenticated HTTP violates the configured origin
1279
1295
  policy.
1280
1296
  """
1281
- 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
+ )
1282
1300
  result = await self._call(
1283
1301
  'activity_stream',
1284
1302
  _methods.build_activity_stream_read_params(options),
@@ -1362,7 +1380,10 @@ class AsyncShotgun:
1362
1380
  type, or if the server result is not a list.
1363
1381
  ProtocolError: If a server request returns an HTTP error.
1364
1382
  ValueError: If authenticated HTTP violates the configured origin
1365
- 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.
1366
1387
  """
1367
1388
  calls = _crud.build_batch_calls(requests)
1368
1389
  if not calls:
@@ -1439,7 +1460,15 @@ class AsyncShotgun:
1439
1460
  )
1440
1461
 
1441
1462
  async def close(self) -> None:
1442
- """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
+ """
1443
1472
  close_after_unlock: list[AsyncTransport] = []
1444
1473
  with self._transport_lock:
1445
1474
  advance_transport_generation(self._config)
@@ -1558,7 +1587,10 @@ class AsyncShotgun:
1558
1587
  file. A thumbnail failure occurs after the entity is created.
1559
1588
  ProtocolError: If a server request returns an HTTP error.
1560
1589
  ValueError: If authenticated HTTP violates the configured origin
1561
- 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.
1562
1594
  """
1563
1595
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=False)
1564
1596
  params = _crud.build_create_params(entity_type, field_data, return_fields)
@@ -1626,13 +1658,13 @@ class AsyncShotgun:
1626
1658
 
1627
1659
  Raises:
1628
1660
  TypeError: If no attachment is given or the reference is a string.
1629
- 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.
1630
1664
  ShotgunError: If another attachment reference is unsupported or a
1631
1665
  setup RPC reports a fault; the fault keeps its specific type.
1632
1666
  ShotgunFileDownloadError: If the resolved URL is invalid or setup,
1633
1667
  transfer, or destination I/O fails.
1634
- ValueError: If server settings are incomplete or authenticated HTTP
1635
- violates the configured origin security policy.
1636
1668
  """
1637
1669
  if attachment is False:
1638
1670
  if attachment_id is None:
@@ -1667,7 +1699,7 @@ class AsyncShotgun:
1667
1699
  headers=headers,
1668
1700
  url=url,
1669
1701
  )
1670
- return await exchange._execute(driver, operation)
1702
+ return await exchange.execute(driver, operation)
1671
1703
  except _errors.ShotgunFileDownloadError:
1672
1704
  raise
1673
1705
  except (TransportError, _errors.ProtocolError, OSError):
@@ -1682,54 +1714,39 @@ class AsyncShotgun:
1682
1714
  """
1683
1715
  raise _methods.entity_types_alias_error()
1684
1716
 
1685
- @overload
1686
1717
  async def export_page(
1687
1718
  self,
1688
1719
  page_id: int,
1689
- page_format: str,
1690
- /,
1720
+ format: str, # noqa: A002
1691
1721
  layout_name: str | None = None,
1692
- ) -> str: ...
1693
-
1694
- @overload
1695
- async def export_page(
1696
- self,
1697
- page_id: int,
1698
- **kwargs: Unpack[_methods.ExportPageKeywordArguments],
1699
- ) -> str: ...
1700
-
1701
- @_methods.expose_export_page_signature
1702
- async def export_page(self, *args: object, **kwargs: object) -> str:
1722
+ ) -> str:
1703
1723
  """Export a page using the documented arguments.
1704
1724
 
1725
+ The Page or its layout must be marked API Exportable in the web UI, or
1726
+ the server rejects the export.
1727
+
1705
1728
  Args:
1706
- *args: Positional values from the documented `export_page`
1707
- signature.
1708
- **kwargs: Keyword values from the documented `export_page`
1709
- signature.
1729
+ page_id: Numeric page identifier.
1730
+ format: Required nonempty export format name.
1731
+ layout_name: Optional layout name, including an empty name.
1710
1732
 
1711
1733
  Returns:
1712
1734
  Exported page text returned by the server.
1713
1735
 
1714
1736
  Raises:
1715
- TypeError: If arguments do not match the documented signature.
1716
1737
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1717
- or if an argument or the server result has the wrong type.
1738
+ or if `page_id`, `format`, or the server result is invalid.
1718
1739
  ProtocolError: If a server request returns an HTTP error.
1719
1740
  ValueError: If authenticated HTTP violates the configured origin
1720
1741
  policy.
1721
1742
  """
1722
- options = _methods.bind_export_page(args, kwargs, 'AsyncShotgun')
1743
+ options = _methods.ExportPageOptions(page_id, format, layout_name)
1723
1744
  result = await self._call(
1724
1745
  'export_page',
1725
1746
  _methods.build_export_page_params(options),
1726
1747
  )
1727
1748
  return _methods.require_string_result(result, 'export_page')
1728
1749
 
1729
- @overload
1730
- async def find(self, entity_type: Never) -> list[dict[str, object]]: ...
1731
-
1732
- @overload
1733
1750
  async def find(
1734
1751
  self,
1735
1752
  entity_type: str,
@@ -1742,33 +1759,50 @@ class AsyncShotgun:
1742
1759
  page: int = 0,
1743
1760
  include_archived_projects: bool = True,
1744
1761
  additional_filter_presets: object = None,
1745
- ) -> list[dict[str, object]]: ...
1746
-
1747
- @_crud.expose_find_signature
1748
- async def find(self, *args: object, **kwargs: object) -> list[dict[str, object]]:
1762
+ ) -> list[dict[str, object]]:
1749
1763
  """Find entities matching the supplied filters.
1750
1764
 
1751
1765
  Args:
1752
- *args: Positional values from the documented `find` signature.
1753
- **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.
1754
1776
 
1755
1777
  Returns:
1756
1778
  Matching entities in server order.
1757
1779
 
1758
1780
  Raises:
1759
- TypeError: If arguments do not match the documented signature.
1760
1781
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1761
- 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.
1762
1784
  ProtocolError: If a server request returns an HTTP error.
1763
1785
  ValueError: If authenticated HTTP violates the configured origin
1764
- 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.
1765
1790
  """
1766
- return await self._find(_crud.bind_find(args, kwargs, 'AsyncShotgun'))
1767
-
1768
- @overload
1769
- 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
+ )
1770
1805
 
1771
- @overload
1772
1806
  async def find_one(
1773
1807
  self,
1774
1808
  entity_type: str,
@@ -1779,28 +1813,43 @@ class AsyncShotgun:
1779
1813
  retired_only: bool = False,
1780
1814
  include_archived_projects: bool = True,
1781
1815
  additional_filter_presets: object = None,
1782
- ) -> dict[str, object] | None: ...
1783
-
1784
- @_crud.expose_find_one_signature
1785
- async def find_one(self, *args: object, **kwargs: object) -> dict[str, object] | None:
1816
+ ) -> dict[str, object] | None:
1786
1817
  """Return the first entity matching the supplied filters.
1787
1818
 
1788
1819
  Args:
1789
- *args: Positional values from the documented `find_one` signature.
1790
- **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.
1791
1828
 
1792
1829
  Returns:
1793
1830
  The first matching entity, or `None` when no entity matches.
1794
1831
 
1795
1832
  Raises:
1796
- TypeError: If arguments do not match the documented signature.
1797
1833
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1798
- 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.
1799
1836
  ProtocolError: If a server request returns an HTTP error.
1800
1837
  ValueError: If authenticated HTTP violates the configured origin
1801
- 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.
1802
1842
  """
1803
- 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
+ )
1804
1853
  return _crud.first_result(await self._find(_crud.find_options_from_one(options)))
1805
1854
 
1806
1855
  async def follow(
@@ -2022,7 +2071,11 @@ class AsyncShotgun:
2022
2071
  entity,
2023
2072
  seed_entity_field,
2024
2073
  )
2025
- result = await self._call('nav_search_entity', params)
2074
+ result = await self._call(
2075
+ 'nav_search',
2076
+ params,
2077
+ _InvocationOptions(logical_method_name='nav_search_entity'),
2078
+ )
2026
2079
  return _methods.require_dictionary_list_result(result, 'nav_search_entity')
2027
2080
 
2028
2081
  async def nav_search_string(
@@ -2053,7 +2106,11 @@ class AsyncShotgun:
2053
2106
  search_string,
2054
2107
  seed_entity_field,
2055
2108
  )
2056
- result = await self._call('nav_search_string', params)
2109
+ result = await self._call(
2110
+ 'nav_search',
2111
+ params,
2112
+ _InvocationOptions(logical_method_name='nav_search_string'),
2113
+ )
2057
2114
  return _methods.require_dictionary_list_result(result, 'nav_search_string')
2058
2115
 
2059
2116
  async def note_thread_read(
@@ -2078,7 +2135,11 @@ class AsyncShotgun:
2078
2135
  policy.
2079
2136
  """
2080
2137
  params = _methods.build_note_thread_read_params(note_id, entity_fields)
2081
- result = await self._call('note_thread_read', params)
2138
+ result = await self._call(
2139
+ 'note_thread_contents',
2140
+ params,
2141
+ _InvocationOptions(logical_method_name='note_thread_read'),
2142
+ )
2082
2143
  return _methods.require_dictionary_list_result(result, 'note_thread_read')
2083
2144
 
2084
2145
  async def preferences_read(self, prefs: list[str] | None = None) -> dict[str, object]:
@@ -2185,7 +2246,10 @@ class AsyncShotgun:
2185
2246
  or if the server result is invalid.
2186
2247
  ProtocolError: If a server request returns an HTTP error.
2187
2248
  ValueError: If authenticated HTTP violates the configured origin
2188
- 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.
2189
2253
  """
2190
2254
  params = _methods.build_schema_field_create_params(
2191
2255
  entity_type,
@@ -2273,7 +2337,10 @@ class AsyncShotgun:
2273
2337
  or if the server result is invalid.
2274
2338
  ProtocolError: If a server request returns an HTTP error.
2275
2339
  ValueError: If authenticated HTTP violates the configured origin
2276
- 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.
2277
2344
  """
2278
2345
  params = _methods.build_schema_field_update_params(
2279
2346
  entity_type,
@@ -2357,7 +2424,8 @@ class AsyncShotgun:
2357
2424
 
2358
2425
  Supply exactly one of `thumbnail_path` and `source_entity`. A local
2359
2426
  thumbnail is uploaded to the first target before it is shared with the
2360
- remaining targets.
2427
+ remaining targets. A local thumbnail is rejected before any upload
2428
+ request if the server requires legacy form uploads.
2361
2429
 
2362
2430
  Args:
2363
2431
  entities: Nonempty list of target entities with `type` and `id`.
@@ -2374,7 +2442,8 @@ class AsyncShotgun:
2374
2442
  TypeError: If forwarded arguments do not match the upload signature
2375
2443
  or include `field_name`.
2376
2444
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2377
- or if the source selection, entities, or transport fails.
2445
+ or if the source selection, entities, or transport fails, or a
2446
+ local upload requires the legacy form route.
2378
2447
  ShotgunThumbnailNotReady: If an uploaded source stays transient.
2379
2448
  ProtocolError: If an upload endpoint returns an HTTP error.
2380
2449
  ValueError: If authenticated HTTP violates the configured origin
@@ -2386,30 +2455,26 @@ class AsyncShotgun:
2386
2455
  source_entity,
2387
2456
  filmstrip_thumbnail,
2388
2457
  )
2389
- if share.thumbnail_path is not None and 'field_name' in kwargs:
2390
- raise TypeError("AsyncShotgun.upload() got multiple values for argument 'field_name'")
2391
2458
  prepared: _files.PreparedUpload | None = None
2392
2459
  if share.thumbnail_path is not None:
2393
- arguments = _bind_upload(
2394
- (
2395
- share.source_entity_type,
2396
- share.source_entity_id,
2397
- share.thumbnail_path,
2398
- ),
2399
- {
2400
- **kwargs,
2401
- 'field_name': (
2402
- 'filmstrip_thumb_image' if filmstrip_thumbnail else 'thumb_image'
2403
- ),
2404
- },
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')),
2405
2474
  )
2406
2475
  prepared = await asyncio.to_thread(
2407
2476
  _files.prepare_upload,
2408
- _files.UploadOptions(
2409
- arguments=arguments,
2410
- upload_type='Thumbnail',
2411
- filmstrip=filmstrip_thumbnail,
2412
- ),
2477
+ _files.upload_options(arguments),
2413
2478
  )
2414
2479
  context = config_module.snapshot(self._config)
2415
2480
  require_secure_rpc(context, include_auth=True)
@@ -2424,9 +2489,7 @@ class AsyncShotgun:
2424
2489
  )
2425
2490
  if len(share.entities) == 1:
2426
2491
  return attachment_id
2427
- field_name: Literal['filmstrip_image', 'image'] = (
2428
- 'filmstrip_image' if filmstrip_thumbnail else 'image'
2429
- )
2492
+ field_name = _files.thumbnail_fields(filmstrip_thumbnail)[1]
2430
2493
  ready = await driver.poll(
2431
2494
  lambda: self._thumbnail_source_ready(
2432
2495
  driver,
@@ -2455,10 +2518,6 @@ class AsyncShotgun:
2455
2518
  filmstrip_thumbnail,
2456
2519
  )
2457
2520
 
2458
- @overload
2459
- async def summarize(self, entity_type: Never) -> dict[str, object]: ...
2460
-
2461
- @overload
2462
2521
  async def summarize(
2463
2522
  self,
2464
2523
  entity_type: str,
@@ -2467,28 +2526,39 @@ class AsyncShotgun:
2467
2526
  filter_operator: str | None = None,
2468
2527
  grouping: list[dict[str, Any]] | None = None,
2469
2528
  include_archived_projects: bool = True,
2470
- ) -> dict[str, object]: ...
2471
-
2472
- @_methods.expose_summarize_signature
2473
- async def summarize(self, *args: object, **kwargs: object) -> dict[str, object]:
2529
+ ) -> dict[str, object]:
2474
2530
  """Summarize field data returned by a query.
2475
2531
 
2476
2532
  Args:
2477
- *args: Positional values from the documented `summarize` signature.
2478
- **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`.
2479
2539
 
2480
2540
  Returns:
2481
- Dict with `summaries` and optional `groups`.
2541
+ Dict with `summaries` and (when grouped) `groups`.
2482
2542
 
2483
2543
  Raises:
2484
- TypeError: If arguments do not match the documented signature.
2485
2544
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2486
- or if arguments or the server result are invalid.
2545
+ or if `filters`, `summary_fields`, or the server result has the
2546
+ wrong type.
2487
2547
  ProtocolError: If a server request returns an HTTP error.
2488
2548
  ValueError: If authenticated HTTP violates the configured origin
2489
- 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.
2490
2553
  """
2491
- 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
+ )
2492
2562
  params = _methods.build_summarize_params(options)
2493
2563
  result = await self._call('summarize', params)
2494
2564
  return _crud.require_dictionary_result(result, 'summarize')
@@ -2516,7 +2586,10 @@ class AsyncShotgun:
2516
2586
  or if arguments or the server result are invalid.
2517
2587
  ProtocolError: If a server request returns an HTTP error.
2518
2588
  ValueError: If authenticated HTTP violates the configured origin
2519
- 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.
2520
2593
  """
2521
2594
  params = _methods.build_text_search_params(
2522
2595
  text,
@@ -2586,7 +2659,10 @@ class AsyncShotgun:
2586
2659
  nonempty file. A thumbnail failure occurs after the update.
2587
2660
  ProtocolError: If a server request returns an HTTP error.
2588
2661
  ValueError: If authenticated HTTP violates the configured origin
2589
- 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.
2590
2666
  """
2591
2667
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=True)
2592
2668
  params = _crud.build_update_params(
@@ -2617,7 +2693,8 @@ class AsyncShotgun:
2617
2693
  user: Optional user entity to attribute the access to.
2618
2694
 
2619
2695
  Raises:
2620
- 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.
2621
2698
  ProtocolError: If a server request returns an HTTP error.
2622
2699
  ValueError: If authenticated HTTP violates the configured origin
2623
2700
  policy.
@@ -2628,10 +2705,6 @@ class AsyncShotgun:
2628
2705
  _InvocationOptions(logical_method_name='update_project_last_accessed'),
2629
2706
  )
2630
2707
 
2631
- @overload
2632
- async def upload(self, entity_type: Never) -> int: ...
2633
-
2634
- @overload
2635
2708
  async def upload(
2636
2709
  self,
2637
2710
  entity_type: str,
@@ -2641,49 +2714,37 @@ class AsyncShotgun:
2641
2714
  display_name: str | None = None,
2642
2715
  tag_list: str | None = None,
2643
2716
  created_at: _dt_module.datetime | None = None,
2644
- ) -> int: ...
2645
-
2646
- @_expose_upload_signature
2647
- async def upload(self, *args: object, **kwargs: object) -> int:
2717
+ ) -> int:
2648
2718
  """Upload a file or thumbnail and return its Attachment ID.
2649
2719
 
2650
- A successful HTTP response from the legacy form-upload route still
2651
- raises `ShotgunError`; no Attachment ID is established for that route.
2652
- The form request has already been sent when this error is raised.
2720
+ A server requiring legacy form uploads raises `ShotgunError` before any
2721
+ upload request is sent. Server capability reads may occur.
2653
2722
 
2654
2723
  Args:
2655
- *args: Positional arguments from the documented upload signature.
2656
- **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.
2657
2732
 
2658
2733
  Returns:
2659
2734
  Created Attachment ID.
2660
2735
 
2661
2736
  Raises:
2662
- TypeError: If arguments do not match the documented signature.
2663
2737
  ShotgunError: If the file or an upload response is invalid, an RPC
2664
- fault or terminal transport failure occurs, or the legacy
2665
- form-upload route succeeds.
2738
+ fault or terminal transport failure occurs, or the server
2739
+ requires legacy form uploads.
2666
2740
  ProtocolError: If an upload endpoint returns an HTTP error.
2667
2741
  ValueError: If authenticated HTTP violates the configured origin
2668
2742
  policy.
2669
2743
  """
2670
- arguments = _bind_upload(args, kwargs)
2671
- thumbnail_fields = {
2672
- 'filmstrip_image',
2673
- 'filmstrip_thumb_image',
2674
- 'image',
2675
- 'thumb_image',
2676
- }
2677
- upload_type: Literal['Attachment', 'Thumbnail'] = (
2678
- 'Thumbnail' if arguments.field_name in thumbnail_fields else 'Attachment'
2679
- )
2680
- return await self._upload(
2681
- _files.UploadOptions(
2682
- arguments=arguments,
2683
- upload_type=upload_type,
2684
- filmstrip=arguments.field_name in {'filmstrip_image', 'filmstrip_thumb_image'},
2685
- )
2744
+ arguments = _files.UploadArguments(
2745
+ entity_type, entity_id, path, field_name, display_name, tag_list, created_at
2686
2746
  )
2747
+ return await self._upload(_files.upload_options(arguments))
2687
2748
 
2688
2749
  async def upload_filmstrip_thumbnail(
2689
2750
  self,
@@ -2694,6 +2755,9 @@ class AsyncShotgun:
2694
2755
  ) -> int:
2695
2756
  """Upload a filmstrip thumbnail through `upload`.
2696
2757
 
2758
+ Servers requiring legacy form uploads are rejected before any upload
2759
+ request is sent.
2760
+
2697
2761
  Args:
2698
2762
  entity_type: Entity type receiving the thumbnail.
2699
2763
  entity_id: Numeric identifier of the receiving entity.
@@ -2706,7 +2770,8 @@ class AsyncShotgun:
2706
2770
  Raises:
2707
2771
  TypeError: If forwarded arguments do not match `upload`.
2708
2772
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2709
- or if the file, upload response, or transport fails.
2773
+ or if the file, upload response, or transport fails, or the
2774
+ server requires legacy form uploads.
2710
2775
  ProtocolError: If an upload endpoint returns an HTTP error.
2711
2776
  ValueError: If authenticated HTTP violates the configured origin
2712
2777
  policy.
@@ -2728,6 +2793,9 @@ class AsyncShotgun:
2728
2793
  ) -> int:
2729
2794
  """Upload a static thumbnail through `upload`.
2730
2795
 
2796
+ Servers requiring legacy form uploads are rejected before any upload
2797
+ request is sent.
2798
+
2731
2799
  Args:
2732
2800
  entity_type: Entity type receiving the thumbnail.
2733
2801
  entity_id: Numeric identifier of the receiving entity.
@@ -2740,7 +2808,8 @@ class AsyncShotgun:
2740
2808
  Raises:
2741
2809
  TypeError: If forwarded arguments do not match `upload`.
2742
2810
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2743
- or if the file, upload response, or transport fails.
2811
+ or if the file, upload response, or transport fails, or the
2812
+ server requires legacy form uploads.
2744
2813
  ProtocolError: If an upload endpoint returns an HTTP error.
2745
2814
  ValueError: If authenticated HTTP violates the configured origin
2746
2815
  policy.
@@ -2756,28 +2825,40 @@ class AsyncShotgun:
2756
2825
  async def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
2757
2826
  """Assign subscriptions to users.
2758
2827
 
2828
+ The server accepts this call from scripts and from human users with
2829
+ permission to manage subscriptions, such as members of the Admin
2830
+ permission group. It refuses other human users.
2831
+
2759
2832
  Args:
2760
- users: Subscription dictionaries containing `user_id` keys.
2833
+ users: Subscription dictionaries with a string or integer
2834
+ `humanUserId`; integers become decimal string keys and booleans
2835
+ are not accepted. Each user may appear only once.
2836
+ `subscription` is `None`, a string, or a list of strings.
2837
+ Lists are copied in order. Other entry keys are ignored.
2761
2838
 
2762
2839
  Returns:
2763
- Whether the server created the subscriptions.
2840
+ `True` when every per-user report is `None`, meaning the server
2841
+ reported no problem; `False` when any user has a non-`None` report.
2764
2842
 
2765
2843
  Raises:
2766
2844
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2767
- or if arguments or the server result are invalid.
2845
+ or if `users` contains invalid or duplicate entries, or the
2846
+ server result is not a mapping with string keys.
2768
2847
  ProtocolError: If a server request returns an HTTP error.
2769
2848
  ValueError: If authenticated HTTP violates the configured origin
2770
2849
  policy.
2771
2850
  """
2772
2851
  params = _methods.build_user_subscriptions_create_params(users)
2773
2852
  result = await self._call('user_subscriptions_create', params)
2774
- return _crud.require_boolean_result(result, 'user_subscriptions_create')
2853
+ return _methods.require_user_subscriptions_create_result(result)
2775
2854
 
2776
2855
  async def user_subscriptions_read(self) -> list[dict[str, object]]:
2777
- """List the current user's subscriptions.
2856
+ """List user subscriptions in server order.
2778
2857
 
2779
2858
  Returns:
2780
- User subscription dictionaries returned by the server.
2859
+ Subscription entries in server order, each with an unchanged string
2860
+ `humanUserId` and a `subscription` of `None`, a string, or a copied
2861
+ list of strings.
2781
2862
 
2782
2863
  Raises:
2783
2864
  ShotgunError: If an RPC fault or terminal transport failure occurs,
@@ -2787,7 +2868,7 @@ class AsyncShotgun:
2787
2868
  policy.
2788
2869
  """
2789
2870
  result = await self._call('user_subscriptions_read', None)
2790
- return _methods.require_dictionary_list_result(result, 'user_subscriptions_read')
2871
+ return _methods.require_user_subscriptions_result(result)
2791
2872
 
2792
2873
  async def work_schedule_read(
2793
2874
  self,
@@ -2823,10 +2904,6 @@ class AsyncShotgun:
2823
2904
  result = await self._call('work_schedule_read', params)
2824
2905
  return _crud.require_dictionary_result(result, 'work_schedule_read')
2825
2906
 
2826
- @overload
2827
- async def work_schedule_update(self, date: Never) -> dict[str, object]: ...
2828
-
2829
- @overload
2830
2907
  async def work_schedule_update(
2831
2908
  self,
2832
2909
  date: _dt_module.date | str,
@@ -2835,37 +2912,49 @@ class AsyncShotgun:
2835
2912
  project: dict[str, Any] | None = None,
2836
2913
  user: dict[str, Any] | None = None,
2837
2914
  recalculate_field: str | None = None,
2838
- ) -> dict[str, object]: ...
2839
-
2840
- @_methods.expose_work_schedule_update_signature
2841
- async def work_schedule_update(
2842
- self,
2843
- *args: object,
2844
- **kwargs: object,
2845
2915
  ) -> dict[str, object]:
2846
- """Update a work schedule using the documented arguments.
2916
+ """Update a work schedule.
2847
2917
 
2848
2918
  Args:
2849
- *args: Positional values from the documented public signature.
2850
- **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.
2851
2925
 
2852
2926
  Returns:
2853
2927
  Updated work schedule data returned by the server.
2854
2928
 
2855
2929
  Raises:
2856
- TypeError: If arguments do not match the documented signature.
2857
2930
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2858
- or if arguments or the server result are invalid.
2931
+ or if the date, project/user exclusivity, or server result is
2932
+ invalid.
2859
2933
  ProtocolError: If a server request returns an HTTP error.
2860
2934
  ValueError: If authenticated HTTP violates the configured origin
2861
2935
  policy.
2862
2936
  """
2863
- options = _methods.bind_work_schedule_update(args, kwargs, 'AsyncShotgun')
2937
+ options = _methods.WorkScheduleUpdateOptions(
2938
+ date, working, description, project, user, recalculate_field
2939
+ )
2864
2940
  params = _methods.build_work_schedule_update_params(options)
2865
2941
  result = await self._call('work_schedule_update', params)
2866
2942
  return _crud.require_dictionary_result(result, 'work_schedule_update')
2867
2943
 
2868
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
+
2869
2958
  def _open_download_temporary(destination: Path) -> tuple[ExitStack, BinaryIO, Path]:
2870
2959
  """Open one adjacent download temporary file."""
2871
2960
  stack = ExitStack()
@@ -2898,7 +2987,6 @@ async def _execute_upload_exchange(
2898
2987
  headers=dict(exchange.headers),
2899
2988
  body=exchange.body_factory(),
2900
2989
  timeout=context.timeout,
2901
- token=uuid.uuid4().hex,
2902
2990
  )
2903
2991
 
2904
2992
  async def consume(