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
shotgun_api3/_client.py CHANGED
@@ -3,16 +3,16 @@
3
3
  import copy
4
4
  import dataclasses
5
5
  import datetime as _dt_module
6
- import inspect
7
6
  import os
8
7
  import threading
9
8
  import urllib.request
10
- import uuid
11
9
  from collections.abc import Callable, Generator, Iterator, Mapping
12
10
  from contextlib import ExitStack, contextmanager, suppress
11
+ from functools import partial
13
12
  from pathlib import Path
14
13
  from typing import (
15
14
  Any,
15
+ ClassVar,
16
16
  Literal,
17
17
  Never,
18
18
  TypeAlias,
@@ -48,7 +48,6 @@ from ._execution import (
48
48
  release_lease,
49
49
  retire_transport,
50
50
  )
51
- from ._signatures import bind_arguments, expose_signature
52
51
  from ._transport import (
53
52
  SendError,
54
53
  StreamingResponse,
@@ -58,154 +57,12 @@ from ._transport import (
58
57
  TransportResponse,
59
58
  Urllib3Transport,
60
59
  )
61
- from ._version import PACKAGE_VERSION
60
+ from ._version import COMPATIBILITY_VERSION
62
61
 
63
62
  _ReadinessKey: TypeAlias = tuple[str, int]
64
63
  _UploadResult = TypeVar('_UploadResult')
65
64
 
66
- __version__ = PACKAGE_VERSION
67
-
68
- _CONSTRUCTOR_PARAMETERS = (
69
- inspect.Parameter('base_url', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
70
- inspect.Parameter(
71
- 'script_name',
72
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
73
- default=None,
74
- annotation=str | None,
75
- ),
76
- inspect.Parameter(
77
- 'api_key',
78
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
79
- default=None,
80
- annotation=str | None,
81
- ),
82
- inspect.Parameter(
83
- 'convert_datetimes_to_utc',
84
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
85
- default=True,
86
- annotation=bool,
87
- ),
88
- inspect.Parameter(
89
- 'http_proxy',
90
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
91
- default=None,
92
- annotation=str | None,
93
- ),
94
- inspect.Parameter(
95
- 'connect',
96
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
97
- default=True,
98
- annotation=bool,
99
- ),
100
- inspect.Parameter(
101
- 'ca_certs',
102
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
103
- default=None,
104
- annotation=str | None,
105
- ),
106
- inspect.Parameter(
107
- 'login',
108
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
109
- default=None,
110
- annotation=str | None,
111
- ),
112
- inspect.Parameter(
113
- 'password',
114
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
115
- default=None,
116
- annotation=str | None,
117
- ),
118
- inspect.Parameter(
119
- 'sudo_as_login',
120
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
121
- default=None,
122
- annotation=str | None,
123
- ),
124
- inspect.Parameter(
125
- 'session_token',
126
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
127
- default=None,
128
- annotation=str | None,
129
- ),
130
- inspect.Parameter(
131
- 'auth_token',
132
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
133
- default=None,
134
- annotation=str | None,
135
- ),
136
- )
137
- _UPLOAD_DOCSTRING = """Upload a file or thumbnail and return its Attachment ID.
138
-
139
- A successful HTTP response from the legacy form-upload route still
140
- raises `ShotgunError`; no Attachment ID is established for that
141
- route. The form request has already been sent when this error is
142
- raised.
143
-
144
- Args:
145
- entity_type: Entity type receiving the upload.
146
- entity_id: Numeric identifier of the receiving entity.
147
- path: Existing non-empty file path as text or filesystem-encoded bytes.
148
- field_name: Optional file field receiving an Attachment.
149
- display_name: Optional Attachment display name.
150
- tag_list: Optional comma-delimited Attachment tags.
151
- created_at: Optional Attachment creation timestamp.
152
-
153
- Returns:
154
- Created Attachment ID.
155
-
156
- Raises:
157
- TypeError: If arguments do not match the documented signature.
158
- ShotgunError: If the file or an upload response is invalid, an RPC
159
- fault or terminal transport failure occurs, or the legacy
160
- form-upload route succeeds.
161
- ProtocolError: If an upload endpoint returns an HTTP error.
162
- ValueError: If authenticated HTTP violates the configured origin policy.
163
- """
164
- _UPLOAD_PARAMETERS = (
165
- inspect.Parameter('entity_type', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
166
- inspect.Parameter('entity_id', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
167
- inspect.Parameter('path', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str | bytes),
168
- inspect.Parameter(
169
- 'field_name',
170
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
171
- default=None,
172
- annotation=str | None,
173
- ),
174
- inspect.Parameter(
175
- 'display_name',
176
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
177
- default=None,
178
- annotation=str | None,
179
- ),
180
- inspect.Parameter(
181
- 'tag_list',
182
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
183
- default=None,
184
- annotation=str | None,
185
- ),
186
- inspect.Parameter(
187
- 'created_at',
188
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
189
- default=None,
190
- annotation=_dt_module.datetime | None,
191
- ),
192
- )
193
- CONSTRUCTOR_SIGNATURE = inspect.Signature(_CONSTRUCTOR_PARAMETERS, return_annotation=None)
194
- _METHOD_CONSTRUCTOR_SIGNATURE = inspect.Signature(
195
- (
196
- inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
197
- *_CONSTRUCTOR_PARAMETERS,
198
- ),
199
- return_annotation=None,
200
- )
201
- _METHOD_UPLOAD_SIGNATURE = inspect.Signature(
202
- (
203
- inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
204
- *_UPLOAD_PARAMETERS,
205
- ),
206
- return_annotation=int,
207
- )
208
- _UPLOAD_SIGNATURE = inspect.Signature(_UPLOAD_PARAMETERS, return_annotation=int)
65
+ __version__ = COMPATIBILITY_VERSION
209
66
 
210
67
 
211
68
  @dataclasses.dataclass(frozen=True)
@@ -274,13 +131,16 @@ class _DownloadExchange:
274
131
  transfer = ExceptionCapture()
275
132
  try:
276
133
  with transfer:
277
- handle = stack.enter_context(_files.download_temporary(destination))
134
+ with _files.download_destination_io(destination):
135
+ handle = stack.enter_context(_files.download_temporary(destination))
278
136
  temporary = Path(handle.name)
279
137
  control.consume_into(
280
138
  chunks,
281
- handle.write,
139
+ partial(_files.write_download_chunk, destination, handle),
282
140
  total_bytes=total_bytes,
283
141
  )
142
+ with _files.download_destination_io(destination):
143
+ handle.flush()
284
144
  opened_stack = stack
285
145
  stack = None
286
146
  opened_cleanup = SuppressedExceptionCapture()
@@ -290,7 +150,8 @@ class _DownloadExchange:
290
150
  raise _DownloadCleanupError from None
291
151
  if opened_cleanup.exception is not None:
292
152
  raise opened_cleanup.exception
293
- temporary.replace(destination)
153
+ with _files.download_destination_io(destination):
154
+ temporary.replace(destination)
294
155
  finally:
295
156
  close_cleanup = SuppressedExceptionCapture()
296
157
  unlink_cleanup = SuppressedExceptionCapture()
@@ -319,12 +180,23 @@ class _DownloadExchange:
319
180
  return control.consume(chunks, total_bytes=total_bytes)
320
181
  return self._write_file(chunks, control, total_bytes)
321
182
 
322
- def _execute(
183
+ def execute(
323
184
  self,
324
185
  driver: Driver,
325
186
  operation: _protocol.OperationDescriptor,
326
187
  ) -> bytes | str:
327
- """Execute the workflow through the common streaming attempt loop."""
188
+ """Execute the workflow through the common streaming attempt loop.
189
+
190
+ Returns:
191
+ Downloaded bytes, or the destination path for a file download.
192
+
193
+ Raises:
194
+ SendError: If required download cleanup fails.
195
+ TransportError: If the attempt loop cannot complete the transfer.
196
+ ShotgunFileDownloadError: If the response or redirect is invalid,
197
+ or creating, writing, flushing, or publishing the destination
198
+ file fails.
199
+ """
328
200
  try:
329
201
  return driver.execute(
330
202
  operation,
@@ -351,16 +223,6 @@ class _InvocationOptions:
351
223
  auth_spec: _protocol.AuthSpec | None = None
352
224
 
353
225
 
354
- def _bind_upload(
355
- args: tuple[object, ...],
356
- kwargs: dict[str, object],
357
- ) -> _files.UploadArguments:
358
- """Bind runtime arguments with upload-specific errors."""
359
- bound = bind_arguments(_UPLOAD_SIGNATURE, args, kwargs, 'Shotgun.upload()')
360
- arguments = cast('Callable[..., _files.UploadArguments]', _files.UploadArguments)
361
- return arguments(**bound.arguments)
362
-
363
-
364
226
  def _execute_upload_exchange(
365
227
  driver: Driver,
366
228
  context: RequestContext,
@@ -376,7 +238,6 @@ def _execute_upload_exchange(
376
238
  headers=dict(exchange.headers),
377
239
  body=exchange.body_factory(),
378
240
  timeout=context.timeout,
379
- token=uuid.uuid4().hex,
380
241
  )
381
242
 
382
243
  def consume(
@@ -402,43 +263,23 @@ def _execute_upload_exchange(
402
263
  )
403
264
 
404
265
 
405
- def _expose_upload_signature(function: Callable[..., object]) -> Callable[..., object]:
406
- """Expose the observed upload signature on its variadic implementation."""
407
- return expose_signature(function, _METHOD_UPLOAD_SIGNATURE, _UPLOAD_DOCSTRING)
408
-
409
-
410
- def bind_constructor(
411
- args: tuple[object, ...],
412
- kwargs: dict[str, object],
413
- client_name: str,
414
- ) -> ConstructorOptions:
415
- """Bind constructor arguments with client-specific errors.
266
+ def build_config(options: ConstructorOptions, *, in_process: bool = False) -> Config:
267
+ """Initialize and validate network-free configuration, auth, and proxy fields.
416
268
 
417
269
  Args:
418
- args: Positional public arguments.
419
- kwargs: Keyword public arguments.
420
- client_name: Public client class name for error attribution.
270
+ options: Constructor values for the site, authentication, and proxy.
271
+ in_process: Whether requests stay inside this process, allowing
272
+ authentication without the network HTTP origin restriction.
421
273
 
422
274
  Returns:
423
- Bound immutable constructor options.
424
-
425
- Raises:
426
- TypeError: If the arguments do not match the public signature.
427
- """
428
- bound = bind_arguments(CONSTRUCTOR_SIGNATURE, args, kwargs, f'{client_name}.__init__()')
429
- constructor = cast('Callable[..., ConstructorOptions]', ConstructorOptions)
430
- return constructor(**bound.arguments)
431
-
432
-
433
- def build_config(options: ConstructorOptions) -> Config:
434
- """Initialize and validate network-free configuration, auth, and proxy fields.
275
+ A new configuration containing the validated constructor settings.
435
276
 
436
277
  Raises:
437
278
  TypeError: If a configuration or proxy value has an unsupported type.
438
279
  ValueError: If authentication, site or proxy values, or
439
280
  `SHOTGUN_API_RETRY_INTERVAL` are invalid.
440
281
  """
441
- config = Config()
282
+ config = Config(_in_process=in_process)
442
283
  config.set_server_params(options.base_url)
443
284
  config.update_fields(
444
285
  api_key=options.api_key,
@@ -457,26 +298,6 @@ def build_config(options: ConstructorOptions) -> Config:
457
298
  return config
458
299
 
459
300
 
460
- def expose_constructor_signature(function: Callable[..., object]) -> Callable[..., object]:
461
- """Expose the documented constructor signature.
462
-
463
- Args:
464
- function: Variadic constructor implementation.
465
-
466
- Returns:
467
- The same function with its public runtime signature attached.
468
- """
469
- expose_signature(function, _METHOD_CONSTRUCTOR_SIGNATURE, None)
470
- annotations = {
471
- name: parameter.annotation
472
- for name, parameter in CONSTRUCTOR_SIGNATURE.parameters.items()
473
- if parameter.annotation is not inspect.Parameter.empty
474
- }
475
- annotations['return'] = CONSTRUCTOR_SIGNATURE.return_annotation
476
- object.__setattr__(function, '__annotations__', annotations)
477
- return function
478
-
479
-
480
301
  class Shotgun:
481
302
  """Provide the synchronous Flow Production Tracking API client surface.
482
303
 
@@ -490,14 +311,12 @@ class Shotgun:
490
311
  config: The writable client configuration.
491
312
  server_caps: Validated capabilities for the configured origin.
492
313
  server_info: Raw validated server metadata.
314
+ transport: Current transport backend, or `None` before connection or
315
+ after retirement.
493
316
  """
494
317
 
495
- __signature__ = CONSTRUCTOR_SIGNATURE
318
+ _in_process: ClassVar[bool] = False
496
319
 
497
- @overload
498
- def __init__(self, base_url: Never) -> None: ...
499
-
500
- @overload
501
320
  def __init__(
502
321
  self,
503
322
  base_url: str,
@@ -512,10 +331,7 @@ class Shotgun:
512
331
  sudo_as_login: str | None = None,
513
332
  session_token: str | None = None,
514
333
  auth_token: str | None = None,
515
- ) -> None: ...
516
-
517
- @expose_constructor_signature
518
- def __init__(self, *args: object, **kwargs: object) -> None:
334
+ ) -> None:
519
335
  """Initialize a client for one site and authentication mode.
520
336
 
521
337
  Supply exactly one authentication mode: `script_name` with `api_key`,
@@ -527,30 +343,60 @@ class Shotgun:
527
343
  `http_proxy` accepts `[user:pass@]host[:port]`, with port 8080 when
528
344
  omitted. A truthy `ca_certs` selects a CA bundle; otherwise
529
345
  `SHOTGUN_API_CACERTS` is consulted before using system trust roots.
346
+ `SHOTGUN_API_RETRY_INTERVAL` supplies a nonnegative integer retry
347
+ interval in milliseconds, defaulting to 3000.
530
348
 
531
349
  Construction performs server-readiness I/O by default. Supplying
532
350
  `auth_token` also performs a session-token exchange during
533
351
  construction, even when `connect` is `False`.
534
352
 
535
353
  Args:
536
- *args: Positional values from the documented constructor signature.
537
- **kwargs: Keyword values from the documented constructor signature.
354
+ base_url: Flow Production Tracking site URL.
355
+ script_name: Script name for script authentication.
356
+ api_key: API key for script authentication.
357
+ convert_datetimes_to_utc: Whether to convert datetime values to
358
+ UTC.
359
+ http_proxy: Optional proxy authority in `[user:pass@]host[:port]`
360
+ form.
361
+ connect: Whether to perform eager readiness during construction.
362
+ ca_certs: Optional certificate-authority bundle path.
363
+ login: Login for human-user authentication.
364
+ password: Password for human-user authentication.
365
+ sudo_as_login: Optional user login for sudo impersonation.
366
+ session_token: Existing session token for authentication.
367
+ auth_token: Optional two-factor authentication token.
538
368
 
539
369
  Raises:
540
- TypeError: If arguments do not match the documented signature.
541
- ValueError: If authentication, URL, or proxy values are invalid.
370
+ ValueError: If authentication, URL, or proxy values are invalid,
371
+ the configured CA bundle is not a readable file, or
372
+ `SHOTGUN_API_RETRY_INTERVAL` is not a nonnegative integer.
542
373
  ShotgunError: If authentication, server metadata, or an RPC result
543
374
  is invalid, or a transport attempt fails.
544
375
  ProtocolError: If eager readiness or token exchange receives an
545
376
  HTTP error.
546
377
  """
547
- self._initialize(bind_constructor(args, kwargs, 'Shotgun'))
378
+ self._initialize(
379
+ ConstructorOptions(
380
+ base_url,
381
+ script_name,
382
+ api_key,
383
+ convert_datetimes_to_utc,
384
+ http_proxy,
385
+ connect,
386
+ ca_certs,
387
+ login,
388
+ password,
389
+ sudo_as_login,
390
+ session_token,
391
+ auth_token,
392
+ )
393
+ )
548
394
 
549
395
  @property
550
396
  def _connection(self) -> Transport | None:
551
397
  """Return the current private connection compatibility view."""
552
398
  with self._transport_lock:
553
- return self._transport
399
+ return self.transport
554
400
 
555
401
  def _invoke(
556
402
  self,
@@ -573,12 +419,16 @@ class Shotgun:
573
419
  return _protocol.transform_inbound(result, context.convert_datetimes_to_utc)
574
420
 
575
421
  def _new_transport(self, context: RequestContext) -> Transport:
576
- """Create a transport for one captured generation."""
422
+ """Create a transport for one generation; mockgun overrides this package-internal hook."""
423
+ if context.in_process:
424
+ raise RuntimeError('an in-process client must supply its own transport')
577
425
  return Urllib3Transport(
578
426
  ca_certs=self._ca_certs,
579
427
  proxy_headers=dict(context.proxy_headers) or None,
580
428
  proxy_url=context.proxy_url,
581
429
  pool_maxsize=context.pool_maxsize,
430
+ max_connection_idle_secs=context.max_connection_idle_secs,
431
+ tcp_keepalive_timeout_secs=context.tcp_keepalive_timeout_secs,
582
432
  )
583
433
 
584
434
  def _publish_capabilities(
@@ -616,34 +466,39 @@ class Shotgun:
616
466
  """Lease the driver and transport for one complete logical operation."""
617
467
  close_after_unlock: list[Transport] = []
618
468
  with self._transport_lock:
619
- if self._known_transport is not self._transport:
469
+ decision = _files.choose_driver_lease(
470
+ self._known_transport,
471
+ self.transport,
472
+ self._driver_generation,
473
+ context.transport_generation,
474
+ self._transport_owned,
475
+ )
476
+ if decision.known_changed:
620
477
  if self._known_transport is not None:
621
478
  retired = retire_transport(
622
479
  self._lease_counts, self._retired_transports, self._known_transport
623
480
  )
624
481
  if retired is not None:
625
482
  close_after_unlock.append(retired)
626
- self._known_transport = self._transport
483
+ self._known_transport = self.transport
627
484
  self._driver = None
628
485
  self._driver_generation = None
629
486
  self._transport_owned = False
630
487
 
631
- transport = self._transport
488
+ transport = self.transport
632
489
  if transport is None:
633
490
  transport = self._new_transport(context)
634
- self._transport = transport
491
+ self.transport = transport
635
492
  self._known_transport = transport
636
493
  self._transport_owned = True
637
- elif self._driver_generation not in {None, context.transport_generation}:
638
- if self._transport_owned:
639
- retired = retire_transport(
640
- self._lease_counts, self._retired_transports, transport
641
- )
642
- if retired is not None:
643
- close_after_unlock.append(retired)
644
- transport = self._new_transport(context)
645
- self._transport = transport
646
- self._known_transport = transport
494
+ elif decision.replace_owned_transport:
495
+ retired = retire_transport(self._lease_counts, self._retired_transports, transport)
496
+ if retired is not None:
497
+ close_after_unlock.append(retired)
498
+ transport = self._new_transport(context)
499
+ self.transport = transport
500
+ self._known_transport = transport
501
+ if decision.reset_driver:
647
502
  self._driver = None
648
503
 
649
504
  if self._driver is None:
@@ -801,7 +656,9 @@ class Shotgun:
801
656
  _files.SHARE_THUMBNAIL,
802
657
  fields,
803
658
  ),
804
- lambda text, _headers: _files.parse_link_completion(text),
659
+ lambda text, _headers: _files.parse_link_completion(
660
+ text, operation='share_thumbnail', entities=entities
661
+ ),
805
662
  )
806
663
 
807
664
  def _thumbnail_source_ready(
@@ -875,34 +732,12 @@ class Shotgun:
875
732
  _files.StorageRange(
876
733
  length=prepared.file_size,
877
734
  offset=0,
878
- path=prepared.options.arguments.path,
735
+ path=prepared.path,
879
736
  ),
880
737
  ),
881
738
  lambda _text, _headers: None,
882
739
  )
883
740
 
884
- @staticmethod
885
- def _upload_form(
886
- driver: Driver,
887
- context: RequestContext,
888
- prepared: _files.PreparedUpload,
889
- ) -> Never:
890
- """Send the observed form request without guessing a success result."""
891
- if context.canonical_origin is None:
892
- raise ValueError('server parameters must be configured before upload I/O')
893
- exchange = _files.build_form_upload_exchange(
894
- context.canonical_origin,
895
- context.user_agent,
896
- prepared,
897
- context.auth,
898
- )
899
-
900
- def reject_success(_text: str, _headers: Mapping[str, str]) -> Never:
901
- raise ShotgunError('form upload success response is not established')
902
-
903
- _execute_upload_exchange(driver, context, exchange, reject_success)
904
- raise AssertionError('form upload response parser returned')
905
-
906
741
  @staticmethod
907
742
  def _upload_link(
908
743
  driver: Driver,
@@ -928,7 +763,16 @@ class Shotgun:
928
763
  _files.LINK_FILE,
929
764
  fields,
930
765
  ),
931
- lambda text, _headers: _files.parse_link_completion(text),
766
+ lambda text, _headers: _files.parse_link_completion(
767
+ text,
768
+ operation='upload link',
769
+ entities=[
770
+ {
771
+ 'type': prepared.options.arguments.entity_type,
772
+ 'id': prepared.options.arguments.entity_id,
773
+ }
774
+ ],
775
+ ),
932
776
  )
933
777
 
934
778
  @staticmethod
@@ -997,16 +841,8 @@ class Shotgun:
997
841
  prepared: _files.PreparedUpload,
998
842
  ) -> int:
999
843
  """Execute one prepared upload through a captured driver lease."""
1000
- arguments = prepared.options.arguments
1001
844
  capabilities = self._ensure_readiness(context, driver)
1002
- upload_path = _files.choose_upload_path(
1003
- capabilities.server_info,
1004
- arguments.entity_type,
1005
- arguments.field_name,
1006
- prepared.file_size,
1007
- )
1008
- if upload_path == 'form':
1009
- return self._upload_form(driver, context, prepared)
845
+ upload_path = _files.choose_prepared_upload_path(capabilities.server_info, prepared)
1010
846
  multipart = upload_path == 'multipart'
1011
847
  upload_info = self._upload_allocation(driver, context, prepared, multipart)
1012
848
  if multipart:
@@ -1030,7 +866,10 @@ class Shotgun:
1030
866
  prepared: _files.PreparedUpload,
1031
867
  ) -> int:
1032
868
  """Execute the exact direct-storage thumbnail workflow."""
1033
- self._ensure_readiness(context, driver)
869
+ capabilities = self._ensure_readiness(context, driver)
870
+ _files.choose_prepared_upload_path(
871
+ capabilities.server_info, prepared, sharing_thumbnail=True
872
+ )
1034
873
  upload_info = self._upload_allocation(driver, context, prepared, multipart=False)
1035
874
  self._upload_direct(driver, context, prepared, upload_info)
1036
875
  return self._upload_link(driver, context, prepared, upload_info)
@@ -1057,10 +896,6 @@ class Shotgun:
1057
896
  result.update({field_name: current.get(field_name) for field_name in thumbnails})
1058
897
  return result
1059
898
 
1060
- @overload
1061
- def activity_stream_read(self, entity_type: Never) -> dict[str, object]: ...
1062
-
1063
- @overload
1064
899
  def activity_stream_read(
1065
900
  self,
1066
901
  entity_type: str,
@@ -1069,30 +904,30 @@ class Shotgun:
1069
904
  min_id: int | None = None,
1070
905
  max_id: int | None = None,
1071
906
  limit: int | None = None,
1072
- ) -> dict[str, object]: ...
1073
-
1074
- @_methods.expose_activity_stream_read_signature
1075
- def activity_stream_read(self, *args: object, **kwargs: object) -> dict[str, object]:
907
+ ) -> dict[str, object]:
1076
908
  """Read the activity stream for one entity.
1077
909
 
1078
910
  Args:
1079
- *args: Positional arguments from the documented activity-stream
1080
- signature.
1081
- **kwargs: Keyword arguments from the documented activity-stream
1082
- signature.
911
+ entity_type: Entity type whose activity stream to read.
912
+ entity_id: Numeric identifier of the entity.
913
+ entity_fields: Optional fields to return for linked entity types.
914
+ min_id: Optional minimum activity identifier.
915
+ max_id: Optional maximum activity identifier.
916
+ limit: Optional maximum number of activity records.
1083
917
 
1084
918
  Returns:
1085
919
  Activity stream data returned by the server.
1086
920
 
1087
921
  Raises:
1088
- TypeError: If arguments do not match the documented signature.
1089
922
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1090
923
  or if the server result has the wrong type.
1091
924
  ProtocolError: If a server request returns an HTTP error.
1092
925
  ValueError: If authenticated HTTP violates the configured origin
1093
926
  policy.
1094
927
  """
1095
- options = _methods.bind_activity_stream_read(args, kwargs, 'Shotgun')
928
+ options = _methods.ActivityStreamReadOptions(
929
+ entity_type, entity_id, entity_fields, min_id, max_id, limit
930
+ )
1096
931
  return _crud.require_dictionary_result(
1097
932
  self._call(
1098
933
  'activity_stream',
@@ -1178,7 +1013,10 @@ class Shotgun:
1178
1013
  type, or if the server result is not a list.
1179
1014
  ProtocolError: If a server request returns an HTTP error.
1180
1015
  ValueError: If authenticated HTTP violates the configured origin
1181
- policy.
1016
+ policy, or a naive datetime cannot be converted to UTC.
1017
+ OverflowError: If converting an aware datetime to UTC exceeds the
1018
+ supported datetime range.
1019
+ OSError: If the platform cannot resolve a local datetime.
1182
1020
  """
1183
1021
  calls = _crud.build_batch_calls(requests)
1184
1022
  if not calls:
@@ -1251,21 +1089,29 @@ class Shotgun:
1251
1089
  )
1252
1090
 
1253
1091
  def close(self) -> None:
1254
- """Retire known and current transports after in-flight work releases them."""
1092
+ """Retire known and current transports after in-flight work releases them.
1093
+
1094
+ Attempt the remaining immediately eligible closes if one fails, then
1095
+ propagate the first failure.
1096
+
1097
+ Raises:
1098
+ Exception: If a transport eligible for immediate cleanup fails to
1099
+ close.
1100
+ """
1255
1101
  close_after_unlock: list[Transport] = []
1256
1102
  with self._transport_lock:
1257
1103
  advance_transport_generation(self._config)
1258
- if self._known_transport is not None and self._known_transport is not self._transport:
1104
+ if self._known_transport is not None and self._known_transport is not self.transport:
1259
1105
  retired = retire_transport(
1260
1106
  self._lease_counts, self._retired_transports, self._known_transport
1261
1107
  )
1262
1108
  if retired is not None:
1263
1109
  close_after_unlock.append(retired)
1264
- transport = self._transport
1110
+ transport = self.transport
1265
1111
  self._driver = None
1266
1112
  self._driver_generation = None
1267
1113
  self._known_transport = None
1268
- self._transport = None
1114
+ self.transport = None
1269
1115
  self._transport_owned = False
1270
1116
  if transport is not None:
1271
1117
  retired = retire_transport(self._lease_counts, self._retired_transports, transport)
@@ -1336,7 +1182,10 @@ class Shotgun:
1336
1182
  file. A thumbnail failure occurs after the entity is created.
1337
1183
  ProtocolError: If a server request returns an HTTP error.
1338
1184
  ValueError: If authenticated HTTP violates the configured origin
1339
- policy.
1185
+ policy, or a naive datetime cannot be converted to UTC.
1186
+ OverflowError: If converting an aware datetime to UTC exceeds the
1187
+ supported datetime range.
1188
+ OSError: If the platform cannot resolve a local datetime.
1340
1189
  """
1341
1190
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=False)
1342
1191
  params = _crud.build_create_params(entity_type, field_data, return_fields)
@@ -1404,13 +1253,13 @@ class Shotgun:
1404
1253
 
1405
1254
  Raises:
1406
1255
  TypeError: If no attachment is given or the reference is a string.
1407
- ValueError: If an entity dictionary references a different type.
1256
+ ValueError: If an entity dictionary references a different type,
1257
+ server settings are incomplete, or authenticated HTTP violates
1258
+ the configured origin security policy.
1408
1259
  ShotgunError: If another attachment reference is unsupported or a
1409
1260
  setup RPC reports a fault; the fault keeps its specific type.
1410
1261
  ShotgunFileDownloadError: If the resolved URL is invalid or setup,
1411
1262
  transfer, or destination I/O fails.
1412
- ValueError: If server settings are incomplete or authenticated HTTP
1413
- violates the configured origin security policy.
1414
1263
  """
1415
1264
  if attachment is False:
1416
1265
  if attachment_id is None:
@@ -1445,7 +1294,7 @@ class Shotgun:
1445
1294
  headers=headers,
1446
1295
  url=url,
1447
1296
  )
1448
- return exchange._execute(driver, operation) # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
1297
+ return exchange.execute(driver, operation)
1449
1298
  except _errors.ShotgunFileDownloadError:
1450
1299
  raise
1451
1300
  except (TransportError, _errors.ProtocolError, OSError):
@@ -1460,51 +1309,36 @@ class Shotgun:
1460
1309
  """
1461
1310
  raise _methods.entity_types_alias_error()
1462
1311
 
1463
- @overload
1464
1312
  def export_page(
1465
1313
  self,
1466
1314
  page_id: int,
1467
- page_format: str,
1468
- /,
1315
+ format: str, # noqa: A002
1469
1316
  layout_name: str | None = None,
1470
- ) -> str: ...
1471
-
1472
- @overload
1473
- def export_page(
1474
- self,
1475
- page_id: int,
1476
- **kwargs: Unpack[_methods.ExportPageKeywordArguments],
1477
- ) -> str: ...
1478
-
1479
- @_methods.expose_export_page_signature
1480
- def export_page(self, *args: object, **kwargs: object) -> str:
1317
+ ) -> str:
1481
1318
  """Export a page using the documented arguments.
1482
1319
 
1320
+ The Page or its layout must be marked API Exportable in the web UI, or
1321
+ the server rejects the export.
1322
+
1483
1323
  Args:
1484
- *args: Positional values from the documented `export_page`
1485
- signature.
1486
- **kwargs: Keyword values from the documented `export_page`
1487
- signature.
1324
+ page_id: Numeric page identifier.
1325
+ format: Required nonempty export format name.
1326
+ layout_name: Optional layout name, including an empty name.
1488
1327
 
1489
1328
  Returns:
1490
1329
  Exported page text returned by the server.
1491
1330
 
1492
1331
  Raises:
1493
- TypeError: If arguments do not match the documented signature.
1494
1332
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1495
- or if an argument or the server result has the wrong type.
1333
+ or if `page_id`, `format`, or the server result is invalid.
1496
1334
  ProtocolError: If a server request returns an HTTP error.
1497
1335
  ValueError: If authenticated HTTP violates the configured origin
1498
1336
  policy.
1499
1337
  """
1500
- options = _methods.bind_export_page(args, kwargs, 'Shotgun')
1338
+ options = _methods.ExportPageOptions(page_id, format, layout_name)
1501
1339
  params = _methods.build_export_page_params(options)
1502
1340
  return _methods.require_string_result(self._call('export_page', params), 'export_page')
1503
1341
 
1504
- @overload
1505
- def find(self, entity_type: Never) -> list[dict[str, object]]: ...
1506
-
1507
- @overload
1508
1342
  def find(
1509
1343
  self,
1510
1344
  entity_type: str,
@@ -1517,33 +1351,50 @@ class Shotgun:
1517
1351
  page: int = 0,
1518
1352
  include_archived_projects: bool = True,
1519
1353
  additional_filter_presets: object = None,
1520
- ) -> list[dict[str, object]]: ...
1521
-
1522
- @_crud.expose_find_signature
1523
- def find(self, *args: object, **kwargs: object) -> list[dict[str, object]]:
1354
+ ) -> list[dict[str, object]]:
1524
1355
  """Find entities matching the supplied filters.
1525
1356
 
1526
1357
  Args:
1527
- *args: Positional values from the documented `find` signature.
1528
- **kwargs: Keyword values from the documented `find` signature.
1358
+ entity_type: Entity type to find.
1359
+ filters: Filters in list or dict syntax.
1360
+ fields: Optional fields to return; defaults to `id`.
1361
+ order: Optional field and direction sort specifications.
1362
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
1363
+ limit: Maximum entities to return, or `0` for no limit.
1364
+ retired_only: Return retired entities instead of active entities.
1365
+ page: Specific page to return, or `0` to read all pages.
1366
+ include_archived_projects: Include entities from archived projects.
1367
+ additional_filter_presets: Optional server-side filter presets.
1529
1368
 
1530
1369
  Returns:
1531
1370
  Matching entities in server order.
1532
1371
 
1533
1372
  Raises:
1534
- TypeError: If arguments do not match the documented signature.
1535
1373
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1536
- or if filters, paging, ordering, or a server result is invalid.
1374
+ or if filters, fields, paging, ordering, or a server result is
1375
+ invalid.
1537
1376
  ProtocolError: If a server request returns an HTTP error.
1538
1377
  ValueError: If authenticated HTTP violates the configured origin
1539
- policy.
1378
+ policy, or a naive datetime cannot be converted to UTC.
1379
+ OverflowError: If converting an aware datetime to UTC exceeds the
1380
+ supported datetime range.
1381
+ OSError: If the platform cannot resolve a local datetime.
1540
1382
  """
1541
- return self._find(_crud.bind_find(args, kwargs, 'Shotgun'))
1542
-
1543
- @overload
1544
- def find_one(self, entity_type: Never) -> dict[str, object] | None: ...
1383
+ return self._find(
1384
+ _crud.FindOptions(
1385
+ entity_type,
1386
+ filters,
1387
+ fields,
1388
+ order,
1389
+ filter_operator,
1390
+ limit,
1391
+ retired_only,
1392
+ page,
1393
+ include_archived_projects,
1394
+ additional_filter_presets,
1395
+ )
1396
+ )
1545
1397
 
1546
- @overload
1547
1398
  def find_one(
1548
1399
  self,
1549
1400
  entity_type: str,
@@ -1554,28 +1405,43 @@ class Shotgun:
1554
1405
  retired_only: bool = False,
1555
1406
  include_archived_projects: bool = True,
1556
1407
  additional_filter_presets: object = None,
1557
- ) -> dict[str, object] | None: ...
1558
-
1559
- @_crud.expose_find_one_signature
1560
- def find_one(self, *args: object, **kwargs: object) -> dict[str, object] | None:
1408
+ ) -> dict[str, object] | None:
1561
1409
  """Return the first entity matching the supplied filters.
1562
1410
 
1563
1411
  Args:
1564
- *args: Positional values from the documented `find_one` signature.
1565
- **kwargs: Keyword values from the documented `find_one` signature.
1412
+ entity_type: Entity type to find.
1413
+ filters: Filters in list or dict syntax.
1414
+ fields: Optional fields to return; defaults to `id`.
1415
+ order: Optional field and direction sort specifications.
1416
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
1417
+ retired_only: Return a retired entity instead of an active entity.
1418
+ include_archived_projects: Include entities from archived projects.
1419
+ additional_filter_presets: Optional server-side filter presets.
1566
1420
 
1567
1421
  Returns:
1568
1422
  The first matching entity, or `None` when no entity matches.
1569
1423
 
1570
1424
  Raises:
1571
- TypeError: If arguments do not match the documented signature.
1572
1425
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1573
- or if filters, paging, ordering, or a server result is invalid.
1426
+ or if filters, fields, paging, ordering, or a server result is
1427
+ invalid.
1574
1428
  ProtocolError: If a server request returns an HTTP error.
1575
1429
  ValueError: If authenticated HTTP violates the configured origin
1576
- policy.
1430
+ policy, or a naive datetime cannot be converted to UTC.
1431
+ OverflowError: If converting an aware datetime to UTC exceeds the
1432
+ supported datetime range.
1433
+ OSError: If the platform cannot resolve a local datetime.
1577
1434
  """
1578
- options = _crud.bind_find_one(args, kwargs, 'Shotgun')
1435
+ options = _crud.FindOneOptions(
1436
+ entity_type,
1437
+ filters,
1438
+ fields,
1439
+ order,
1440
+ filter_operator,
1441
+ retired_only,
1442
+ include_archived_projects,
1443
+ additional_filter_presets,
1444
+ )
1579
1445
  return _crud.first_result(self._find(_crud.find_options_from_one(options)))
1580
1446
 
1581
1447
  def follow(self, user: dict[str, Any], entity: dict[str, Any]) -> dict[str, object]:
@@ -1725,16 +1591,20 @@ class Shotgun:
1725
1591
  def _initialize(self, options: ConstructorOptions) -> None:
1726
1592
  """Initialize client state from validated constructor options."""
1727
1593
  self._base_url = options.base_url.rstrip('/')
1728
- self._ca_certs = options.ca_certs or os.environ.get('SHOTGUN_API_CACERTS') or None
1594
+ self._ca_certs = (
1595
+ options.ca_certs or os.environ.get('SHOTGUN_API_CACERTS') or None
1596
+ if self._in_process
1597
+ else config_module.constructor_ca_certs(options.ca_certs)
1598
+ )
1729
1599
  self._client_caps = ClientCapabilities()
1730
- self._config = build_config(options)
1600
+ self._config = build_config(options, in_process=self._in_process)
1731
1601
 
1732
1602
  self._driver: Driver | None = None
1733
1603
  self._driver_generation: int | None = None
1734
1604
  self._known_transport: Transport | None = None
1735
1605
  self._lease_counts: dict[int, int] = {}
1736
1606
  self._retired_transports: dict[int, Transport] = {}
1737
- self._transport: Transport | None = None
1607
+ self.transport: Transport | None = None
1738
1608
  self._transport_lock = threading.Lock()
1739
1609
  self._transport_owned = False
1740
1610
 
@@ -1827,7 +1697,11 @@ class Shotgun:
1827
1697
  seed_entity_field,
1828
1698
  )
1829
1699
  return _methods.require_dictionary_list_result(
1830
- self._call('nav_search_entity', params),
1700
+ self._call(
1701
+ 'nav_search',
1702
+ params,
1703
+ invocation_options=_InvocationOptions(logical_method_name='nav_search_entity'),
1704
+ ),
1831
1705
  'nav_search_entity',
1832
1706
  )
1833
1707
 
@@ -1860,7 +1734,11 @@ class Shotgun:
1860
1734
  seed_entity_field,
1861
1735
  )
1862
1736
  return _methods.require_dictionary_list_result(
1863
- self._call('nav_search_string', params),
1737
+ self._call(
1738
+ 'nav_search',
1739
+ params,
1740
+ invocation_options=_InvocationOptions(logical_method_name='nav_search_string'),
1741
+ ),
1864
1742
  'nav_search_string',
1865
1743
  )
1866
1744
 
@@ -1887,7 +1765,11 @@ class Shotgun:
1887
1765
  """
1888
1766
  params = _methods.build_note_thread_read_params(note_id, entity_fields)
1889
1767
  return _methods.require_dictionary_list_result(
1890
- self._call('note_thread_read', params),
1768
+ self._call(
1769
+ 'note_thread_contents',
1770
+ params,
1771
+ invocation_options=_InvocationOptions(logical_method_name='note_thread_read'),
1772
+ ),
1891
1773
  'note_thread_read',
1892
1774
  )
1893
1775
 
@@ -2000,7 +1882,10 @@ class Shotgun:
2000
1882
  or if the server result is invalid.
2001
1883
  ProtocolError: If a server request returns an HTTP error.
2002
1884
  ValueError: If authenticated HTTP violates the configured origin
2003
- policy.
1885
+ policy, or a naive datetime cannot be converted to UTC.
1886
+ OverflowError: If converting an aware datetime to UTC exceeds the
1887
+ supported datetime range.
1888
+ OSError: If the platform cannot resolve a local datetime.
2004
1889
  """
2005
1890
  result = self._call(
2006
1891
  'schema_field_create',
@@ -2096,7 +1981,10 @@ class Shotgun:
2096
1981
  or if the server result is invalid.
2097
1982
  ProtocolError: If a server request returns an HTTP error.
2098
1983
  ValueError: If authenticated HTTP violates the configured origin
2099
- policy.
1984
+ policy, or a naive datetime cannot be converted to UTC.
1985
+ OverflowError: If converting an aware datetime to UTC exceeds the
1986
+ supported datetime range.
1987
+ OSError: If the platform cannot resolve a local datetime.
2100
1988
  """
2101
1989
  return _crud.require_boolean_result(
2102
1990
  self._call(
@@ -2185,7 +2073,8 @@ class Shotgun:
2185
2073
 
2186
2074
  Supply exactly one of `thumbnail_path` and `source_entity`. A local
2187
2075
  thumbnail is uploaded to the first target before it is shared with the
2188
- remaining targets.
2076
+ remaining targets. A local thumbnail is rejected before any upload
2077
+ request if the server requires legacy form uploads.
2189
2078
 
2190
2079
  Args:
2191
2080
  entities: Nonempty list of target entities with `type` and `id`.
@@ -2202,7 +2091,8 @@ class Shotgun:
2202
2091
  TypeError: If forwarded arguments do not match the upload signature
2203
2092
  or include `field_name`.
2204
2093
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2205
- or if the source selection, entities, or transport fails.
2094
+ or if the source selection, entities, or transport fails, or a
2095
+ local upload requires the legacy form route.
2206
2096
  ShotgunThumbnailNotReady: If an uploaded source stays transient.
2207
2097
  ProtocolError: If an upload endpoint returns an HTTP error.
2208
2098
  ValueError: If authenticated HTTP violates the configured origin
@@ -2214,30 +2104,23 @@ class Shotgun:
2214
2104
  source_entity,
2215
2105
  filmstrip_thumbnail,
2216
2106
  )
2217
- if share.thumbnail_path is not None and 'field_name' in kwargs:
2218
- raise TypeError("Shotgun.upload() got multiple values for argument 'field_name'")
2219
2107
  prepared: _files.PreparedUpload | None = None
2220
2108
  if share.thumbnail_path is not None:
2221
- arguments = _bind_upload(
2222
- (
2223
- share.source_entity_type,
2224
- share.source_entity_id,
2225
- share.thumbnail_path,
2226
- ),
2227
- {
2228
- **kwargs,
2229
- 'field_name': (
2230
- 'filmstrip_thumb_image' if filmstrip_thumbnail else 'thumb_image'
2231
- ),
2232
- },
2233
- )
2234
- prepared = _files.prepare_upload(
2235
- _files.UploadOptions(
2236
- arguments=arguments,
2237
- upload_type='Thumbnail',
2238
- filmstrip=filmstrip_thumbnail,
2239
- )
2109
+ for keyword in kwargs:
2110
+ if keyword not in ('display_name', 'tag_list', 'created_at'):
2111
+ raise TypeError(
2112
+ f"Shotgun.share_thumbnail() got an unexpected keyword argument '{keyword}'"
2113
+ )
2114
+ arguments = _files.UploadArguments(
2115
+ entity_type=share.source_entity_type,
2116
+ entity_id=share.source_entity_id,
2117
+ path=share.thumbnail_path,
2118
+ field_name=_files.thumbnail_fields(filmstrip_thumbnail)[0],
2119
+ display_name=cast('str | None', kwargs.get('display_name')),
2120
+ tag_list=cast('str | None', kwargs.get('tag_list')),
2121
+ created_at=cast('_dt_module.datetime | None', kwargs.get('created_at')),
2240
2122
  )
2123
+ prepared = _files.prepare_upload(_files.upload_options(arguments))
2241
2124
  context = config_module.snapshot(self._config)
2242
2125
  require_secure_rpc(context, include_auth=True)
2243
2126
  with self._driver_lease(context) as driver:
@@ -2245,9 +2128,7 @@ class Shotgun:
2245
2128
  attachment_id = self._upload_thumbnail_for(driver, context, prepared)
2246
2129
  if len(share.entities) == 1:
2247
2130
  return attachment_id
2248
- field_name: Literal['filmstrip_image', 'image'] = (
2249
- 'filmstrip_image' if filmstrip_thumbnail else 'image'
2250
- )
2131
+ field_name = _files.thumbnail_fields(filmstrip_thumbnail)[1]
2251
2132
  ready = driver.poll(
2252
2133
  lambda: self._thumbnail_source_ready(
2253
2134
  driver,
@@ -2277,10 +2158,6 @@ class Shotgun:
2277
2158
  filmstrip_thumbnail,
2278
2159
  )
2279
2160
 
2280
- @overload
2281
- def summarize(self, entity_type: Never) -> dict[str, object]: ...
2282
-
2283
- @overload
2284
2161
  def summarize(
2285
2162
  self,
2286
2163
  entity_type: str,
@@ -2289,29 +2166,39 @@ class Shotgun:
2289
2166
  filter_operator: str | None = None,
2290
2167
  grouping: list[dict[str, Any]] | None = None,
2291
2168
  include_archived_projects: bool = True,
2292
- ) -> dict[str, object]: ...
2293
-
2294
- @_methods.expose_summarize_signature
2295
- def summarize(self, *args: object, **kwargs: object) -> dict[str, object]:
2169
+ ) -> dict[str, object]:
2296
2170
  """Summarize field data returned by a query.
2297
2171
 
2298
2172
  Args:
2299
- *args: Positional values from the documented `summarize` signature.
2300
- **kwargs: Keyword values from the documented `summarize` signature.
2173
+ entity_type: Entity type to summarize.
2174
+ filters: Filters in list or dict syntax.
2175
+ summary_fields: List of `{'field': ..., 'type': ...}` summaries.
2176
+ filter_operator: `'all'` or `'any'` for list-syntax filters.
2177
+ grouping: Optional grouping specs.
2178
+ include_archived_projects: Include archived projects when `True`.
2301
2179
 
2302
2180
  Returns:
2303
2181
  Dict with `summaries` and (when grouped) `groups`.
2304
2182
 
2305
2183
  Raises:
2306
- TypeError: If arguments do not match the documented signature.
2307
2184
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2308
2185
  or if `filters`, `summary_fields`, or the server result has the
2309
2186
  wrong type.
2310
2187
  ProtocolError: If a server request returns an HTTP error.
2311
2188
  ValueError: If authenticated HTTP violates the configured origin
2312
- policy.
2189
+ policy, or a naive datetime cannot be converted to UTC.
2190
+ OverflowError: If converting an aware datetime to UTC exceeds the
2191
+ supported datetime range.
2192
+ OSError: If the platform cannot resolve a local datetime.
2313
2193
  """
2314
- options = _methods.bind_summarize(args, kwargs, 'Shotgun')
2194
+ options = _methods.SummarizeOptions(
2195
+ entity_type,
2196
+ filters,
2197
+ summary_fields,
2198
+ filter_operator,
2199
+ grouping,
2200
+ include_archived_projects,
2201
+ )
2315
2202
  params = _methods.build_summarize_params(options)
2316
2203
  return _crud.require_dictionary_result(self._call('summarize', params), 'summarize')
2317
2204
 
@@ -2322,7 +2209,7 @@ class Shotgun:
2322
2209
  project_ids: list[int] | None = None,
2323
2210
  limit: int | None = None,
2324
2211
  ) -> dict[str, object]:
2325
- """Search entity display names matching `text` (min 3 chars server-side).
2212
+ """Search entity display names matching `text`.
2326
2213
 
2327
2214
  Args:
2328
2215
  text: Text to match.
@@ -2336,10 +2223,14 @@ class Shotgun:
2336
2223
 
2337
2224
  Raises:
2338
2225
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2339
- or if `entity_types` or the server result is not a dictionary.
2226
+ if `entity_types` or the server result is not a dictionary, or
2227
+ if an entity filter is invalid.
2340
2228
  ProtocolError: If a server request returns an HTTP error.
2341
2229
  ValueError: If authenticated HTTP violates the configured origin
2342
- policy.
2230
+ policy, or a naive datetime cannot be converted to UTC.
2231
+ OverflowError: If converting an aware datetime to UTC exceeds the
2232
+ supported datetime range.
2233
+ OSError: If the platform cannot resolve a local datetime.
2343
2234
  """
2344
2235
  params = _methods.build_text_search_params(
2345
2236
  text,
@@ -2407,7 +2298,10 @@ class Shotgun:
2407
2298
  nonempty file. A thumbnail failure occurs after the update.
2408
2299
  ProtocolError: If a server request returns an HTTP error.
2409
2300
  ValueError: If authenticated HTTP violates the configured origin
2410
- policy.
2301
+ policy, or a naive datetime cannot be converted to UTC.
2302
+ OverflowError: If converting an aware datetime to UTC exceeds the
2303
+ supported datetime range.
2304
+ OSError: If the platform cannot resolve a local datetime.
2411
2305
  """
2412
2306
  field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=True)
2413
2307
  params = _crud.build_update_params(
@@ -2436,7 +2330,8 @@ class Shotgun:
2436
2330
  user: Optional user entity to attribute the access to.
2437
2331
 
2438
2332
  Raises:
2439
- ShotgunError: If an RPC fault or terminal transport failure occurs.
2333
+ ShotgunError: If a project or user ID is invalid, or an RPC fault
2334
+ or terminal transport failure occurs.
2440
2335
  ProtocolError: If a server request returns an HTTP error.
2441
2336
  ValueError: If authenticated HTTP violates the configured origin
2442
2337
  policy.
@@ -2449,10 +2344,6 @@ class Shotgun:
2449
2344
  ),
2450
2345
  )
2451
2346
 
2452
- @overload
2453
- def upload(self, entity_type: Never) -> int: ...
2454
-
2455
- @overload
2456
2347
  def upload(
2457
2348
  self,
2458
2349
  entity_type: str,
@@ -2462,50 +2353,37 @@ class Shotgun:
2462
2353
  display_name: str | None = None,
2463
2354
  tag_list: str | None = None,
2464
2355
  created_at: _dt_module.datetime | None = None,
2465
- ) -> int: ...
2466
-
2467
- @_expose_upload_signature
2468
- def upload(self, *args: object, **kwargs: object) -> int:
2356
+ ) -> int:
2469
2357
  """Upload a file or thumbnail and return its Attachment ID.
2470
2358
 
2471
- A successful HTTP response from the legacy form-upload route still
2472
- raises `ShotgunError`; no Attachment ID is established for that route.
2473
- The form request has already been sent when this error is raised.
2359
+ A server requiring legacy form uploads raises `ShotgunError` before any
2360
+ upload request is sent. Server capability reads may occur.
2474
2361
 
2475
2362
  Args:
2476
- *args: Positional arguments from the documented upload signature;
2477
- the path may be text or filesystem-encoded bytes.
2478
- **kwargs: Keyword arguments from the documented upload signature.
2363
+ entity_type: Entity type receiving the upload.
2364
+ entity_id: Numeric identifier of the receiving entity.
2365
+ path: Existing non-empty file path as text or filesystem-encoded
2366
+ bytes.
2367
+ field_name: Optional file field receiving an Attachment.
2368
+ display_name: Optional Attachment display name.
2369
+ tag_list: Optional comma-delimited Attachment tags.
2370
+ created_at: Optional Attachment creation timestamp.
2479
2371
 
2480
2372
  Returns:
2481
2373
  Created Attachment ID.
2482
2374
 
2483
2375
  Raises:
2484
- TypeError: If arguments do not match the documented signature.
2485
2376
  ShotgunError: If the file or an upload response is invalid, an RPC
2486
- fault or terminal transport failure occurs, or the legacy
2487
- form-upload route succeeds.
2377
+ fault or terminal transport failure occurs, or the server
2378
+ requires legacy form uploads.
2488
2379
  ProtocolError: If an upload endpoint returns an HTTP error.
2489
2380
  ValueError: If authenticated HTTP violates the configured origin
2490
2381
  policy.
2491
2382
  """
2492
- arguments = _bind_upload(args, kwargs)
2493
- thumbnail_fields = {
2494
- 'filmstrip_image',
2495
- 'filmstrip_thumb_image',
2496
- 'image',
2497
- 'thumb_image',
2498
- }
2499
- upload_type: Literal['Attachment', 'Thumbnail'] = (
2500
- 'Thumbnail' if arguments.field_name in thumbnail_fields else 'Attachment'
2501
- )
2502
- return self._upload(
2503
- _files.UploadOptions(
2504
- arguments=arguments,
2505
- upload_type=upload_type,
2506
- filmstrip=arguments.field_name in {'filmstrip_image', 'filmstrip_thumb_image'},
2507
- )
2383
+ arguments = _files.UploadArguments(
2384
+ entity_type, entity_id, path, field_name, display_name, tag_list, created_at
2508
2385
  )
2386
+ return self._upload(_files.upload_options(arguments))
2509
2387
 
2510
2388
  def upload_filmstrip_thumbnail(
2511
2389
  self,
@@ -2516,6 +2394,9 @@ class Shotgun:
2516
2394
  ) -> int:
2517
2395
  """Upload a filmstrip thumbnail through `upload`.
2518
2396
 
2397
+ Servers requiring legacy form uploads are rejected before any upload
2398
+ request is sent.
2399
+
2519
2400
  Args:
2520
2401
  entity_type: Entity type receiving the thumbnail.
2521
2402
  entity_id: Numeric identifier of the receiving entity.
@@ -2528,7 +2409,8 @@ class Shotgun:
2528
2409
  Raises:
2529
2410
  TypeError: If forwarded arguments do not match `upload`.
2530
2411
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2531
- or if the file, upload response, or transport fails.
2412
+ or if the file, upload response, or transport fails, or the
2413
+ server requires legacy form uploads.
2532
2414
  ProtocolError: If an upload endpoint returns an HTTP error.
2533
2415
  ValueError: If authenticated HTTP violates the configured origin
2534
2416
  policy.
@@ -2550,6 +2432,9 @@ class Shotgun:
2550
2432
  ) -> int:
2551
2433
  """Upload a static thumbnail through `upload`.
2552
2434
 
2435
+ Servers requiring legacy form uploads are rejected before any upload
2436
+ request is sent.
2437
+
2553
2438
  Args:
2554
2439
  entity_type: Entity type receiving the thumbnail.
2555
2440
  entity_id: Numeric identifier of the receiving entity.
@@ -2562,7 +2447,8 @@ class Shotgun:
2562
2447
  Raises:
2563
2448
  TypeError: If forwarded arguments do not match `upload`.
2564
2449
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2565
- or if the file, upload response, or transport fails.
2450
+ or if the file, upload response, or transport fails, or the
2451
+ server requires legacy form uploads.
2566
2452
  ProtocolError: If an upload endpoint returns an HTTP error.
2567
2453
  ValueError: If authenticated HTTP violates the configured origin
2568
2454
  policy.
@@ -2578,44 +2464,53 @@ class Shotgun:
2578
2464
  def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
2579
2465
  """Assign subscriptions to users.
2580
2466
 
2467
+ The server accepts this call from scripts and from human users with
2468
+ permission to manage subscriptions, such as members of the Admin
2469
+ permission group. It refuses other human users.
2470
+
2581
2471
  Args:
2582
- users: Subscription dictionaries that each contain a `user_id` key.
2472
+ users: Subscription dictionaries with a string or integer
2473
+ `humanUserId`; integers become decimal string keys and booleans
2474
+ are not accepted. Each user may appear only once.
2475
+ `subscription` is `None`, a string, or a list of strings.
2476
+ Lists are copied in order. Other entry keys are ignored.
2583
2477
 
2584
2478
  Returns:
2585
- Whether the server created the subscriptions.
2479
+ `True` when every per-user report is `None`, meaning the server
2480
+ reported no problem; `False` when any user has a non-`None` report.
2586
2481
 
2587
2482
  Raises:
2588
2483
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2589
- or if `users` is not a list of dictionaries that each contain a
2590
- `user_id` key, or the server result is not a boolean.
2484
+ or if `users` contains invalid or duplicate entries, or the
2485
+ server result is not a mapping with string keys.
2591
2486
  ProtocolError: If a server request returns an HTTP error.
2592
2487
  ValueError: If authenticated HTTP violates the configured origin
2593
2488
  policy.
2594
2489
  """
2595
- return _crud.require_boolean_result(
2490
+ return _methods.require_user_subscriptions_create_result(
2596
2491
  self._call(
2597
2492
  'user_subscriptions_create',
2598
2493
  _methods.build_user_subscriptions_create_params(users),
2599
2494
  ),
2600
- 'user_subscriptions_create',
2601
2495
  )
2602
2496
 
2603
2497
  def user_subscriptions_read(self) -> list[dict[str, object]]:
2604
- """List the current user's subscriptions.
2498
+ """List user subscriptions in server order.
2605
2499
 
2606
2500
  Returns:
2607
- User subscription dictionaries returned by the server.
2501
+ Subscription entries in server order, each with an unchanged string
2502
+ `humanUserId` and a `subscription` of `None`, a string, or a copied
2503
+ list of strings.
2608
2504
 
2609
2505
  Raises:
2610
2506
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2611
- or if the server result is not a list of dictionaries.
2507
+ or if the server result is invalid.
2612
2508
  ProtocolError: If a server request returns an HTTP error.
2613
2509
  ValueError: If authenticated HTTP violates the configured origin
2614
2510
  policy.
2615
2511
  """
2616
- return _methods.require_dictionary_list_result(
2512
+ return _methods.require_user_subscriptions_result(
2617
2513
  self._call('user_subscriptions_read', None),
2618
- 'user_subscriptions_read',
2619
2514
  )
2620
2515
 
2621
2516
  def work_schedule_read(
@@ -2654,10 +2549,6 @@ class Shotgun:
2654
2549
  'work_schedule_read',
2655
2550
  )
2656
2551
 
2657
- @overload
2658
- def work_schedule_update(self, date: Never) -> dict[str, object]: ...
2659
-
2660
- @overload
2661
2552
  def work_schedule_update(
2662
2553
  self,
2663
2554
  date: _dt_module.date | str,
@@ -2666,30 +2557,31 @@ class Shotgun:
2666
2557
  project: dict[str, Any] | None = None,
2667
2558
  user: dict[str, Any] | None = None,
2668
2559
  recalculate_field: str | None = None,
2669
- ) -> dict[str, object]: ...
2670
-
2671
- @_methods.expose_work_schedule_update_signature
2672
- def work_schedule_update(self, *args: object, **kwargs: object) -> dict[str, object]:
2673
- """Update a work schedule using the documented arguments.
2560
+ ) -> dict[str, object]:
2561
+ """Update a work schedule.
2674
2562
 
2675
2563
  Args:
2676
- *args: Positional values from the documented `work_schedule_update`
2677
- signature.
2678
- **kwargs: Keyword values from the documented `work_schedule_update`
2679
- signature.
2564
+ date: Date or exact `YYYY-MM-DD` string to update.
2565
+ working: Whether the date is a working day.
2566
+ description: Optional schedule description.
2567
+ project: Optional project entity restriction.
2568
+ user: Optional user entity restriction.
2569
+ recalculate_field: Optional field to recalculate.
2680
2570
 
2681
2571
  Returns:
2682
2572
  Updated work schedule data returned by the server.
2683
2573
 
2684
2574
  Raises:
2685
- TypeError: If arguments do not match the documented signature.
2686
2575
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2687
- or if dates, exclusivity, or the server result is invalid.
2576
+ or if the date, project/user exclusivity, or server result is
2577
+ invalid.
2688
2578
  ProtocolError: If a server request returns an HTTP error.
2689
2579
  ValueError: If authenticated HTTP violates the configured origin
2690
2580
  policy.
2691
2581
  """
2692
- options = _methods.bind_work_schedule_update(args, kwargs, 'Shotgun')
2582
+ options = _methods.WorkScheduleUpdateOptions(
2583
+ date, working, description, project, user, recalculate_field
2584
+ )
2693
2585
  params = _methods.build_work_schedule_update_params(options)
2694
2586
  return _crud.require_dictionary_result(
2695
2587
  self._call('work_schedule_update', params),