sg-python-api 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sg-python-api
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python client for Autodesk Flow Production Tracking
5
5
  Project-URL: Documentation, https://github.com/dactylo/sg-python-api/tree/main/docs
6
6
  Project-URL: Homepage, https://github.com/dactylo/sg-python-api
@@ -44,9 +44,9 @@ This documentation uses SG for Flow Production Tracking, formerly named Shotgun
44
44
  for application tests.
45
45
 
46
46
  sg-python-api is an independent implementation of the documented API of Autodesk's Python client
47
- `3.10.2`. Some behavior differs on purpose, so validate your application's workflows against a test
48
- site before replacing its production client. The [migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/migration-guide.md) walks
49
- through the switch, and the [compatibility reference](https://github.com/dactylo/sg-python-api/blob/v0.1.0/COMPATIBILITY.md) lists the differences.
47
+ `3.10.3`. Some behavior differs on purpose, so validate your application's workflows against a test
48
+ site before replacing its production client. The [migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/migration-guide.md) walks
49
+ through the switch, and the [compatibility reference](https://github.com/dactylo/sg-python-api/blob/v0.1.2/COMPATIBILITY.md) lists the differences.
50
50
 
51
51
  ## Installation
52
52
 
@@ -55,16 +55,9 @@ requests to a remote site require HTTPS.
55
55
 
56
56
  `sg-python-api` and Autodesk's `shotgun-api3` both install the `shotgun_api3` package, so install
57
57
  exactly one of them in a Python environment. If the official client is already installed, follow
58
- the [migration steps](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/migration-guide.md#install-the-replacement) to replace it.
58
+ the [migration steps](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/migration-guide.md#install-the-replacement) to replace it.
59
59
 
60
- When a release is available on your package index, install into the Python environment that runs
61
- your application:
62
-
63
- ```sh
64
- python -m pip install sg-python-api
65
- ```
66
-
67
- To install from a checkout instead, run:
60
+ From a checkout, install into the Python environment that runs your application:
68
61
 
69
62
  ```sh
70
63
  python -m pip install .
@@ -74,12 +67,6 @@ The base installation provides the synchronous `Shotgun` client and both `mockgu
74
67
  use the native asynchronous client, `AsyncShotgun`, install the `async` extra, which adds the
75
68
  `httpx2` HTTP library:
76
69
 
77
- ```sh
78
- python -m pip install 'sg-python-api[async]'
79
- ```
80
-
81
- From a checkout, use:
82
-
83
70
  ```sh
84
71
  python -m pip install '.[async]'
85
72
  ```
@@ -143,23 +130,23 @@ operation fails. Without the `async` extra, importing `AsyncShotgun` raises `Imp
143
130
 
144
131
  Share an `AsyncShotgun` only among tasks on one event loop, and cancel its work by cancelling the
145
132
  task. A cancelled write may already have reached the server, so check its outcome before repeating
146
- it. The [async reference](https://github.com/dactylo/sg-python-api/blob/v0.1.0/COMPATIBILITY.md#async-extension-surfaces) covers shutdown and
133
+ it. The [async reference](https://github.com/dactylo/sg-python-api/blob/v0.1.2/COMPATIBILITY.md#async-extension-surfaces) covers shutdown and
147
134
  cancellation in detail.
148
135
 
149
136
  ## Documentation
150
137
 
151
138
  | If you want to... | Read |
152
139
  | ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
153
- | Replace Autodesk's client in an existing application | [Migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/migration-guide.md) |
154
- | Check what behaves differently from Autodesk's client | [Compatibility](https://github.com/dactylo/sg-python-api/blob/v0.1.0/COMPATIBILITY.md) |
155
- | Tune pooling and page prefetch, or observe requests | [Configuration extensions](https://github.com/dactylo/sg-python-api/blob/v0.1.0/COMPATIBILITY.md#configuration-and-observer-extensions) |
156
- | Confirm which client loads and run a smoke test | [Testing an installation](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/testing.md) |
157
- | Compare measured latency and transfer memory | [Benchmarks](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/benchmarks.md) |
158
- | See changes by version | [Changelog](https://github.com/dactylo/sg-python-api/blob/v0.1.0/CHANGELOG.md) |
140
+ | Replace Autodesk's client in an existing application | [Migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/migration-guide.md) |
141
+ | Check what behaves differently from Autodesk's client | [Compatibility](https://github.com/dactylo/sg-python-api/blob/v0.1.2/COMPATIBILITY.md) |
142
+ | Tune pooling and page prefetch, or observe requests | [Configuration extensions](https://github.com/dactylo/sg-python-api/blob/v0.1.2/COMPATIBILITY.md#configuration-and-observer-extensions) |
143
+ | Confirm which client loads and run a smoke test | [Testing an installation](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/testing.md) |
144
+ | Compare measured latency and transfer memory | [Benchmarks](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/benchmarks.md) |
145
+ | See changes by version | [Changelog](https://github.com/dactylo/sg-python-api/blob/v0.1.2/CHANGELOG.md) |
159
146
 
160
147
  ## Development
161
148
 
162
- See the [development guide](https://github.com/dactylo/sg-python-api/blob/v0.1.0/docs/development.md) for the required tools, CI, and dependency locks.
149
+ See the [development guide](https://github.com/dactylo/sg-python-api/blob/v0.1.2/docs/development.md) for the required tools, CI, and dependency locks.
163
150
  With those tools installed, run:
164
151
 
165
152
  ```sh
@@ -172,6 +159,6 @@ make check
172
159
 
173
160
  ## License
174
161
 
175
- sg-python-api is available under the [MIT License](https://github.com/dactylo/sg-python-api/blob/v0.1.0/LICENSE). It is an independent project and is
162
+ sg-python-api is available under the [MIT License](https://github.com/dactylo/sg-python-api/blob/v0.1.2/LICENSE). It is an independent project and is
176
163
  not affiliated with or endorsed by Autodesk. Flow Production Tracking is a product of Autodesk,
177
164
  Inc.
@@ -1,23 +1,23 @@
1
1
  shotgun_api3/__init__.py,sha256=fviJGOi_Gb9jHi5Q9ty5tIbeIvxMtLNrKx_4HJVgemI,2011
2
- shotgun_api3/_async_client.py,sha256=or11Le6Y2WB3TVTVj_7QiLfOLpDbwjs1p3VVAQhFHLQ,111070
3
- shotgun_api3/_async_driver.py,sha256=ldrETCNgihOcuoGYVqt8M4u2R1euNkvp3TRXLYQbzJ0,32838
2
+ shotgun_api3/_async_client.py,sha256=UYVk_IGzMcUguJ4oCV-Nof4mL2kvzYYGuiYvW4gHeMs,113167
3
+ shotgun_api3/_async_driver.py,sha256=NnuMmQP5KrAoANBvWIhcIUe_ljD62pK_7xSIoEIltXw,32308
4
4
  shotgun_api3/_async_httpx2.py,sha256=3pjeFsb5racGArvlR50c3w0XtrttXuRU_6E9Y-cBU3c,24387
5
5
  shotgun_api3/_async_transport.py,sha256=8iAcoFafEHKRBirBkqdIqMFgqLs1YTgtzgACudD0kM4,4928
6
6
  shotgun_api3/_capabilities.py,sha256=RrStvExVC3TAADhfbY2JpYZUXmvE63vXIJy1R-nkEII,5527
7
- shotgun_api3/_client.py,sha256=n4lbXG9UxQhrv1IhwsJd8dD41JIq_Xh-_zqDueSguW8,100184
8
- shotgun_api3/_config.py,sha256=hCKJtaO06wZFJEz-yIw2SagDeWqZMHpuqSmS33qG2Q4,55264
7
+ shotgun_api3/_client.py,sha256=jtemxhdhFr1gwE7qB6K-62Sohb2C_S4dEO7uKpVh2Uo,102109
8
+ shotgun_api3/_config.py,sha256=fALxy5xGI3A0nECsRDyls_9A8tvjB0DNSpTu1agNEHw,54931
9
9
  shotgun_api3/_crud.py,sha256=Ax36jA2E-lauRf2D-2rmRDj61SilOygtkjKgs5XDCUs,30319
10
- shotgun_api3/_driver.py,sha256=sPkiseKisJTMSNAxkCvdGXc_H1kKS6sg-rJQLKXSVgU,30139
10
+ shotgun_api3/_driver.py,sha256=DAtnZW044TWiqscs0G1uwTiluAFtCoR2wKTbJYJHT2g,29498
11
11
  shotgun_api3/_errors.py,sha256=fk7v3XbmZK63M272r5wDwF_GgI-4RynPfAWv1P-J-GI,1480
12
12
  shotgun_api3/_execution.py,sha256=Kn2Yd9sXIA975stnof7MeuA8bT3dyOuWDIqQnP65LO0,8235
13
- shotgun_api3/_files.py,sha256=a1PfHKV4OMi6j7BQG2rkMPCcaNTB5uAWNiqCh64AEXg,35223
14
- shotgun_api3/_methods.py,sha256=8dD5BUN_5hnGAYomUEAX1dHZnI4Cliid6PrrP8AhhPI,36132
15
- shotgun_api3/_protocol.py,sha256=dKr1XqB6Xo56QloZ2MAyN4ulqYkTjw8VYeJRd5QHufQ,45286
13
+ shotgun_api3/_files.py,sha256=-eXHyG0U9mEAdYTthgVEhlzBKYUOt4g-3k9D522L3zM,33900
14
+ shotgun_api3/_methods.py,sha256=pYzx2pN0nus6vPWf-Gl8Zz4MgRf8R4b7kvxN8lhoXgc,37685
15
+ shotgun_api3/_protocol.py,sha256=0ccL65aIm3v2w6dOtNnZuFrKQncA8WO4yadtxRP55-w,42194
16
16
  shotgun_api3/_sentinel.py,sha256=KAQzQjJVhTTZPLFY1f88-_vP7U06i2g6ZS7KJcn4vIA,116
17
17
  shotgun_api3/_sg_timezone.py,sha256=yr7Wdln9fwfTVGMQSFsgO-eduuTHQ5EbSY5xEVQ2tYw,115
18
18
  shotgun_api3/_signatures.py,sha256=_yhNnbkVF-0opaBfg9H6SauBN2RT4hJsHGa3ZO3Ug4s,1622
19
19
  shotgun_api3/_transport.py,sha256=LmQ0e2VvAKwFdNy4SyfRHwgKC6GWnwkIN5Wa2fin3AM,30047
20
- shotgun_api3/_version.py,sha256=yK498PuHvOUCFnIC1QKSqvFXzUBBuOMHBsVo5gSpFCE,102
20
+ shotgun_api3/_version.py,sha256=0NYED9oy2nGCvTERMwWJvajshS5qzH_v4w7QUAGAivY,102
21
21
  shotgun_api3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  shotgun_api3/shotgun.py,sha256=X7PLlCC-gR8dJgY_uLYXw_LWN8mz5MsQsSYJldtavTk,1706
23
23
  shotgun_api3/lib/__init__.py,sha256=4WGYod6xHcj23rapx5bTDGdjT52H1CAQNMz4BmzNmn0,55
@@ -25,7 +25,7 @@ shotgun_api3/lib/sgtimezone.py,sha256=nYSaCsIeq_SJh7YgL4yWN7dTVVz9lAfx-G3mzsHb8i
25
25
  shotgun_api3/lib/mockgun/__init__.py,sha256=2mHjMJAV4mZ8JvnP8WHn6tkdMJoB7UQtNSs99YzNW30,703
26
26
  shotgun_api3/lib/mockgun/_async.py,sha256=7bTZFdlSzeoGq9fRbPBAf-X08qDmrkzV9rxskx5AGX8,10812
27
27
  shotgun_api3/lib/mockgun/_mockgun.py,sha256=k2cqLMCKpU5oIbUGiYulf1aIU0HLrMB6DQQ3KrCy7WQ,65093
28
- sg_python_api-0.1.0.dist-info/METADATA,sha256=7Wzieh6ix8BwZJ_oD-B4_G-bO6m5pPDr1wzC5EFYhUQ,7789
29
- sg_python_api-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
30
- sg_python_api-0.1.0.dist-info/licenses/LICENSE,sha256=V3moMsQxKDchTd3mtXl5J-ODaNBDvFrapOf1DK7Nx_U,1082
31
- sg_python_api-0.1.0.dist-info/RECORD,,
28
+ sg_python_api-0.1.2.dist-info/METADATA,sha256=2Zv6g9pJZ28QttHMO6Q1yYWvyczPypCHc-QCAYbjIaQ,7587
29
+ sg_python_api-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
30
+ sg_python_api-0.1.2.dist-info/licenses/LICENSE,sha256=V3moMsQxKDchTd3mtXl5J-ODaNBDvFrapOf1DK7Nx_U,1082
31
+ sg_python_api-0.1.2.dist-info/RECORD,,
@@ -51,7 +51,6 @@ from ._client import (
51
51
  expose_constructor_signature,
52
52
  )
53
53
  from ._config import (
54
- CompatibilityWarningCode,
55
54
  Config,
56
55
  RequestContext,
57
56
  advance_transport_generation,
@@ -60,7 +59,6 @@ from ._config import (
60
59
  server_host,
61
60
  server_page_limit,
62
61
  )
63
- from ._errors import ShotgunError
64
62
  from ._execution import SuppressedExceptionCapture, release_lease, retire_transport
65
63
  from ._signatures import bind_arguments, expose_signature
66
64
  from ._transport import (
@@ -288,7 +286,6 @@ class _InvocationOptions:
288
286
  """Collect non-payload controls for one client invocation."""
289
287
 
290
288
  auth_spec: _protocol.AuthSpec | None = None
291
- compatibility_warning_codes: tuple[CompatibilityWarningCode, ...] = ()
292
289
  include_auth: bool = True
293
290
  logical_method_name: str | None = None
294
291
 
@@ -523,7 +520,6 @@ class AsyncShotgun:
523
520
  outbound,
524
521
  context,
525
522
  include_auth=invocation.options.include_auth,
526
- compatibility_warning_codes=invocation.options.compatibility_warning_codes,
527
523
  _logical_method_name=invocation.options.logical_method_name,
528
524
  )
529
525
  return _protocol.transform_inbound(result, context.convert_datetimes_to_utc)
@@ -1040,29 +1036,6 @@ class AsyncShotgun:
1040
1036
  lambda _text, _headers: None,
1041
1037
  )
1042
1038
 
1043
- @staticmethod
1044
- async def _upload_form(
1045
- driver: AsyncDriver,
1046
- context: RequestContext,
1047
- prepared: _files.PreparedUpload,
1048
- ) -> Never:
1049
- """Send the observed form request without guessing a success result."""
1050
- if context.canonical_origin is None:
1051
- raise ValueError('server parameters must be configured before upload I/O')
1052
- exchange = _files.build_form_upload_exchange(
1053
- context.canonical_origin,
1054
- context.user_agent,
1055
- prepared,
1056
- context.auth,
1057
- chunk_size=_ASYNC_FILE_BATCH_SIZE,
1058
- )
1059
-
1060
- def reject_success(_text: str, _headers: Mapping[str, str]) -> Never:
1061
- raise ShotgunError('form upload success response is not established')
1062
-
1063
- await _execute_upload_exchange(driver, context, exchange, reject_success)
1064
- raise AssertionError('form upload response parser returned')
1065
-
1066
1039
  @staticmethod
1067
1040
  async def _upload_link(
1068
1041
  driver: AsyncDriver,
@@ -1165,14 +1138,14 @@ class AsyncShotgun:
1165
1138
  arguments = prepared.options.arguments
1166
1139
  capabilities = await self._ensure_readiness(context, driver)
1167
1140
  operation_context = await self._authenticated_context(context, driver)
1168
- upload_path = _files.choose_upload_path(
1169
- capabilities.server_info,
1170
- arguments.entity_type,
1171
- arguments.field_name,
1172
- prepared.file_size,
1141
+ upload_path = _files.require_supported_upload_path(
1142
+ _files.choose_upload_path(
1143
+ capabilities.server_info,
1144
+ arguments.entity_type,
1145
+ arguments.field_name,
1146
+ prepared.file_size,
1147
+ )
1173
1148
  )
1174
- if upload_path == 'form':
1175
- return await self._upload_form(driver, operation_context, prepared)
1176
1149
  multipart = upload_path == 'multipart'
1177
1150
  upload_info = await self._upload_allocation(
1178
1151
  driver,
@@ -1211,6 +1184,16 @@ class AsyncShotgun:
1211
1184
  prepared: _files.PreparedUpload,
1212
1185
  ) -> int:
1213
1186
  """Execute the exact direct-storage thumbnail workflow."""
1187
+ capabilities = await self._ensure_readiness(context, driver)
1188
+ arguments = prepared.options.arguments
1189
+ _files.require_supported_upload_path(
1190
+ _files.choose_upload_path(
1191
+ capabilities.server_info,
1192
+ arguments.entity_type,
1193
+ arguments.field_name,
1194
+ prepared.file_size,
1195
+ )
1196
+ )
1214
1197
  upload_info = await self._upload_allocation(
1215
1198
  driver,
1216
1199
  context,
@@ -1220,6 +1203,28 @@ class AsyncShotgun:
1220
1203
  await self._upload_direct(driver, context, prepared, upload_info)
1221
1204
  return await self._upload_link(driver, context, prepared, upload_info)
1222
1205
 
1206
+ async def _upload_thumbnails_after_write(
1207
+ self,
1208
+ entity_type: str,
1209
+ entity_id: int,
1210
+ thumbnails: dict[str, object],
1211
+ result: dict[str, object],
1212
+ ) -> dict[str, object]:
1213
+ """Upload separated thumbnail paths and merge their current field values."""
1214
+ if not thumbnails:
1215
+ return result
1216
+ for field_name, path in thumbnails.items():
1217
+ upload = (
1218
+ self.upload_filmstrip_thumbnail
1219
+ if field_name == 'filmstrip_image'
1220
+ else self.upload_thumbnail
1221
+ )
1222
+ await upload(entity_type, entity_id, cast('str', path))
1223
+ current = await self.find_one(entity_type, [['id', 'is', entity_id]], list(thumbnails))
1224
+ if current is not None:
1225
+ result.update({field_name: current.get(field_name) for field_name in thumbnails})
1226
+ return result
1227
+
1223
1228
  @overload
1224
1229
  async def activity_stream_read(self, entity_type: Never) -> dict[str, object]: ...
1225
1230
 
@@ -1520,6 +1525,10 @@ class AsyncShotgun:
1520
1525
  ) -> dict[str, object]:
1521
1526
  """Create an entity with the supplied field values.
1522
1527
 
1528
+ Non-empty `image` and `filmstrip_image` values are file paths. They are
1529
+ uploaded as thumbnails after the entity is created, and the result
1530
+ includes their current field values.
1531
+
1523
1532
  Args:
1524
1533
  entity_type: Entity type to create.
1525
1534
  data: Field values for the new entity.
@@ -1530,14 +1539,22 @@ class AsyncShotgun:
1530
1539
 
1531
1540
  Raises:
1532
1541
  ShotgunError: If an RPC fault or terminal transport failure occurs,
1533
- or if `data`, its field names, `return_fields`, or the server
1534
- result is invalid.
1542
+ if `data`, its field names, `return_fields`, or the server
1543
+ result is invalid, or if a thumbnail path is not a nonempty
1544
+ file. A thumbnail failure occurs after the entity is created.
1535
1545
  ProtocolError: If a server request returns an HTTP error.
1536
1546
  ValueError: If authenticated HTTP violates the configured origin
1537
1547
  policy.
1538
1548
  """
1539
- params = _crud.build_create_params(entity_type, data, return_fields)
1540
- return _crud.require_dictionary_result(await self._call('create', params), 'create')
1549
+ field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=False)
1550
+ params = _crud.build_create_params(entity_type, field_data, return_fields)
1551
+ result = _crud.require_dictionary_result(await self._call('create', params), 'create')
1552
+ if not thumbnails:
1553
+ return result
1554
+ entity_id = _crud.require_entity_id(result.get('id'), 'create did not return an entity id')
1555
+ return await self._upload_thumbnails_after_write(
1556
+ entity_type, entity_id, thumbnails, result
1557
+ )
1541
1558
 
1542
1559
  async def delete(self, entity_type: str, entity_id: int) -> bool:
1543
1560
  """Retire an entity.
@@ -1642,6 +1659,15 @@ class AsyncShotgun:
1642
1659
  except (TransportError, _errors.ProtocolError, OSError):
1643
1660
  raise _errors.ShotgunFileDownloadError('attachment download failed') from None
1644
1661
 
1662
+ async def entity_types(self) -> Never:
1663
+ """Reject the deprecated alias of `schema_entity_read()` without I/O.
1664
+
1665
+ Raises:
1666
+ ShotgunError: Always, naming `schema_entity_read()` as the
1667
+ replacement.
1668
+ """
1669
+ raise _methods.entity_types_alias_error()
1670
+
1645
1671
  @overload
1646
1672
  async def export_page(
1647
1673
  self,
@@ -1662,6 +1688,9 @@ class AsyncShotgun:
1662
1688
  async def export_page(self, *args: object, **kwargs: object) -> str:
1663
1689
  """Export a page using the documented arguments.
1664
1690
 
1691
+ The Page or its layout must be marked API Exportable in the web UI, or
1692
+ the server rejects the export.
1693
+
1665
1694
  Args:
1666
1695
  *args: Positional values from the documented `export_page`
1667
1696
  signature.
@@ -1982,7 +2011,11 @@ class AsyncShotgun:
1982
2011
  entity,
1983
2012
  seed_entity_field,
1984
2013
  )
1985
- result = await self._call('nav_search_entity', params)
2014
+ result = await self._call(
2015
+ 'nav_search',
2016
+ params,
2017
+ _InvocationOptions(logical_method_name='nav_search_entity'),
2018
+ )
1986
2019
  return _methods.require_dictionary_list_result(result, 'nav_search_entity')
1987
2020
 
1988
2021
  async def nav_search_string(
@@ -2013,7 +2046,11 @@ class AsyncShotgun:
2013
2046
  search_string,
2014
2047
  seed_entity_field,
2015
2048
  )
2016
- result = await self._call('nav_search_string', params)
2049
+ result = await self._call(
2050
+ 'nav_search',
2051
+ params,
2052
+ _InvocationOptions(logical_method_name='nav_search_string'),
2053
+ )
2017
2054
  return _methods.require_dictionary_list_result(result, 'nav_search_string')
2018
2055
 
2019
2056
  async def note_thread_read(
@@ -2038,7 +2075,11 @@ class AsyncShotgun:
2038
2075
  policy.
2039
2076
  """
2040
2077
  params = _methods.build_note_thread_read_params(note_id, entity_fields)
2041
- result = await self._call('note_thread_read', params)
2078
+ result = await self._call(
2079
+ 'note_thread_contents',
2080
+ params,
2081
+ _InvocationOptions(logical_method_name='note_thread_read'),
2082
+ )
2042
2083
  return _methods.require_dictionary_list_result(result, 'note_thread_read')
2043
2084
 
2044
2085
  async def preferences_read(self, prefs: list[str] | None = None) -> dict[str, object]:
@@ -2085,6 +2126,18 @@ class AsyncShotgun:
2085
2126
  params = _crud.build_entity_action_params(entity_type, entity_id)
2086
2127
  return _crud.require_boolean_result(await self._call('revive', params), 'revive')
2087
2128
 
2129
+ async def schema(self, entity_type: str) -> Never:
2130
+ """Reject the deprecated alias of `schema_field_read()` without I/O.
2131
+
2132
+ Args:
2133
+ entity_type: Entity type named in the replacement suggestion.
2134
+
2135
+ Raises:
2136
+ ShotgunError: Always, naming the equivalent `schema_field_read()`
2137
+ call.
2138
+ """
2139
+ raise _methods.schema_alias_error(entity_type)
2140
+
2088
2141
  async def schema_entity_read(
2089
2142
  self,
2090
2143
  project_entity: dict[str, Any] | None = None,
@@ -2104,34 +2157,12 @@ class AsyncShotgun:
2104
2157
  ValueError: If authenticated HTTP violates the configured origin
2105
2158
  policy.
2106
2159
  """
2107
- warnings_codes = _methods.consume_compatibility_warnings(
2108
- _methods.SCHEMA_ENTITY_READ_WARNINGS,
2109
- self,
2110
- )
2111
2160
  result = await self._call(
2112
2161
  'schema_entity_read',
2113
2162
  _methods.build_schema_entity_read_params(project_entity),
2114
- _InvocationOptions(compatibility_warning_codes=warnings_codes),
2115
2163
  )
2116
2164
  return _crud.require_dictionary_result(result, 'schema_entity_read')
2117
2165
 
2118
- async def entity_types(self) -> dict[str, object]:
2119
- """Return entity schema through the deprecated alias.
2120
-
2121
- Returns:
2122
- Entity-type schema data returned by the server.
2123
-
2124
- Raises:
2125
- ShotgunError: If an RPC fault or terminal transport failure occurs,
2126
- or if the server result is not a dictionary.
2127
- ProtocolError: If a server request returns an HTTP error.
2128
- ValueError: If authenticated HTTP violates the configured origin
2129
- policy.
2130
- """
2131
- _methods.warn_entity_types_alias()
2132
- with _methods.entity_types_alias(self):
2133
- return await self.schema_entity_read()
2134
-
2135
2166
  async def schema_field_create(
2136
2167
  self,
2137
2168
  entity_type: str,
@@ -2210,10 +2241,6 @@ class AsyncShotgun:
2210
2241
  ValueError: If authenticated HTTP violates the configured origin
2211
2242
  policy.
2212
2243
  """
2213
- warning_codes = _methods.consume_compatibility_warnings(
2214
- _methods.SCHEMA_FIELD_READ_WARNINGS,
2215
- self,
2216
- )
2217
2244
  result = await self._call(
2218
2245
  'schema_field_read',
2219
2246
  _methods.build_schema_field_read_params(
@@ -2221,30 +2248,9 @@ class AsyncShotgun:
2221
2248
  field_name,
2222
2249
  project_entity,
2223
2250
  ),
2224
- _InvocationOptions(compatibility_warning_codes=warning_codes),
2225
2251
  )
2226
2252
  return _crud.require_dictionary_result(result, 'schema_field_read')
2227
2253
 
2228
- async def schema(self, entity_type: str) -> dict[str, object]:
2229
- """Return field schema through the deprecated alias.
2230
-
2231
- Args:
2232
- entity_type: Entity type whose fields to return.
2233
-
2234
- Returns:
2235
- Field schema data returned by the server.
2236
-
2237
- Raises:
2238
- ShotgunError: If an RPC fault or terminal transport failure occurs,
2239
- or if the server result is invalid.
2240
- ProtocolError: If a server request returns an HTTP error.
2241
- ValueError: If authenticated HTTP violates the configured origin
2242
- policy.
2243
- """
2244
- _methods.warn_schema_alias()
2245
- with _methods.schema_alias(self):
2246
- return await self.schema_field_read(entity_type)
2247
-
2248
2254
  async def schema_field_update(
2249
2255
  self,
2250
2256
  entity_type: str,
@@ -2352,7 +2358,8 @@ class AsyncShotgun:
2352
2358
 
2353
2359
  Supply exactly one of `thumbnail_path` and `source_entity`. A local
2354
2360
  thumbnail is uploaded to the first target before it is shared with the
2355
- remaining targets.
2361
+ remaining targets. A local thumbnail is rejected before any upload
2362
+ request if the server requires legacy form uploads.
2356
2363
 
2357
2364
  Args:
2358
2365
  entities: Nonempty list of target entities with `type` and `id`.
@@ -2369,7 +2376,8 @@ class AsyncShotgun:
2369
2376
  TypeError: If forwarded arguments do not match the upload signature
2370
2377
  or include `field_name`.
2371
2378
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2372
- or if the source selection, entities, or transport fails.
2379
+ or if the source selection, entities, or transport fails, or a
2380
+ local upload requires the legacy form route.
2373
2381
  ShotgunThumbnailNotReady: If an uploaded source stays transient.
2374
2382
  ProtocolError: If an upload endpoint returns an HTTP error.
2375
2383
  ValueError: If authenticated HTTP violates the configured origin
@@ -2559,6 +2567,12 @@ class AsyncShotgun:
2559
2567
  ) -> dict[str, object]:
2560
2568
  """Update fields on an existing entity.
2561
2569
 
2570
+ Non-empty `image` and `filmstrip_image` values are file paths. They are
2571
+ uploaded as thumbnails after the other fields are updated, and the
2572
+ result includes their current field values. An `image` of `None` clears
2573
+ both thumbnails. When no other fields remain, no update request is sent
2574
+ and the result contains only `id` and `type`.
2575
+
2562
2576
  Args:
2563
2577
  entity_type: Entity type to update.
2564
2578
  entity_id: Numeric entity identifier.
@@ -2570,19 +2584,29 @@ class AsyncShotgun:
2570
2584
 
2571
2585
  Raises:
2572
2586
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2573
- or if `data`, its field names, `multi_entity_update_modes`, or
2574
- the server result is invalid.
2587
+ if `data`, its field names, `multi_entity_update_modes`, or the
2588
+ server result is invalid, or if a thumbnail path is not a
2589
+ nonempty file. A thumbnail failure occurs after the update.
2575
2590
  ProtocolError: If a server request returns an HTTP error.
2576
2591
  ValueError: If authenticated HTTP violates the configured origin
2577
2592
  policy.
2578
2593
  """
2594
+ field_data, thumbnails = _files.split_thumbnail_uploads(data, keep_image_none=True)
2579
2595
  params = _crud.build_update_params(
2580
2596
  entity_type,
2581
2597
  entity_id,
2582
- data,
2598
+ field_data,
2583
2599
  multi_entity_update_modes,
2584
2600
  )
2585
- return _crud.require_dictionary_result(await self._call('update', params), 'update')
2601
+ result: dict[str, object] = {'id': entity_id, 'type': entity_type}
2602
+ if params['fields']:
2603
+ result = _crud.require_dictionary_result(
2604
+ await self._call('update', params),
2605
+ 'update',
2606
+ )
2607
+ return await self._upload_thumbnails_after_write(
2608
+ entity_type, entity_id, thumbnails, result
2609
+ )
2586
2610
 
2587
2611
  async def update_project_last_accessed(
2588
2612
  self,
@@ -2626,9 +2650,8 @@ class AsyncShotgun:
2626
2650
  async def upload(self, *args: object, **kwargs: object) -> int:
2627
2651
  """Upload a file or thumbnail and return its Attachment ID.
2628
2652
 
2629
- A successful HTTP response from the legacy form-upload route still
2630
- raises `ShotgunError`; no Attachment ID is established for that route.
2631
- The form request has already been sent when this error is raised.
2653
+ A server requiring legacy form uploads raises `ShotgunError` before any
2654
+ upload request is sent. Server capability reads may occur.
2632
2655
 
2633
2656
  Args:
2634
2657
  *args: Positional arguments from the documented upload signature.
@@ -2640,8 +2663,8 @@ class AsyncShotgun:
2640
2663
  Raises:
2641
2664
  TypeError: If arguments do not match the documented signature.
2642
2665
  ShotgunError: If the file or an upload response is invalid, an RPC
2643
- fault or terminal transport failure occurs, or the legacy
2644
- form-upload route succeeds.
2666
+ fault or terminal transport failure occurs, or the server
2667
+ requires legacy form uploads.
2645
2668
  ProtocolError: If an upload endpoint returns an HTTP error.
2646
2669
  ValueError: If authenticated HTTP violates the configured origin
2647
2670
  policy.
@@ -2673,6 +2696,9 @@ class AsyncShotgun:
2673
2696
  ) -> int:
2674
2697
  """Upload a filmstrip thumbnail through `upload`.
2675
2698
 
2699
+ Servers requiring legacy form uploads are rejected before any upload
2700
+ request is sent.
2701
+
2676
2702
  Args:
2677
2703
  entity_type: Entity type receiving the thumbnail.
2678
2704
  entity_id: Numeric identifier of the receiving entity.
@@ -2685,7 +2711,8 @@ class AsyncShotgun:
2685
2711
  Raises:
2686
2712
  TypeError: If forwarded arguments do not match `upload`.
2687
2713
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2688
- or if the file, upload response, or transport fails.
2714
+ or if the file, upload response, or transport fails, or the
2715
+ server requires legacy form uploads.
2689
2716
  ProtocolError: If an upload endpoint returns an HTTP error.
2690
2717
  ValueError: If authenticated HTTP violates the configured origin
2691
2718
  policy.
@@ -2707,6 +2734,9 @@ class AsyncShotgun:
2707
2734
  ) -> int:
2708
2735
  """Upload a static thumbnail through `upload`.
2709
2736
 
2737
+ Servers requiring legacy form uploads are rejected before any upload
2738
+ request is sent.
2739
+
2710
2740
  Args:
2711
2741
  entity_type: Entity type receiving the thumbnail.
2712
2742
  entity_id: Numeric identifier of the receiving entity.
@@ -2719,7 +2749,8 @@ class AsyncShotgun:
2719
2749
  Raises:
2720
2750
  TypeError: If forwarded arguments do not match `upload`.
2721
2751
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2722
- or if the file, upload response, or transport fails.
2752
+ or if the file, upload response, or transport fails, or the
2753
+ server requires legacy form uploads.
2723
2754
  ProtocolError: If an upload endpoint returns an HTTP error.
2724
2755
  ValueError: If authenticated HTTP violates the configured origin
2725
2756
  policy.
@@ -2735,28 +2766,40 @@ class AsyncShotgun:
2735
2766
  async def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
2736
2767
  """Assign subscriptions to users.
2737
2768
 
2769
+ The server accepts this call from scripts and from human users with
2770
+ permission to manage subscriptions, such as members of the Admin
2771
+ permission group. It refuses other human users.
2772
+
2738
2773
  Args:
2739
- users: Subscription dictionaries containing `user_id` keys.
2774
+ users: Subscription dictionaries with a string or integer
2775
+ `humanUserId`; integers become decimal string keys and booleans
2776
+ are not accepted. Each user may appear only once.
2777
+ `subscription` is `None`, a string, or a list of strings.
2778
+ Lists are copied in order. Other entry keys are ignored.
2740
2779
 
2741
2780
  Returns:
2742
- Whether the server created the subscriptions.
2781
+ `True` when every per-user report is `None`, meaning the server
2782
+ reported no problem; `False` when any user has a non-`None` report.
2743
2783
 
2744
2784
  Raises:
2745
2785
  ShotgunError: If an RPC fault or terminal transport failure occurs,
2746
- or if arguments or the server result are invalid.
2786
+ or if `users` contains invalid or duplicate entries, or the
2787
+ server result is not a mapping with string keys.
2747
2788
  ProtocolError: If a server request returns an HTTP error.
2748
2789
  ValueError: If authenticated HTTP violates the configured origin
2749
2790
  policy.
2750
2791
  """
2751
2792
  params = _methods.build_user_subscriptions_create_params(users)
2752
2793
  result = await self._call('user_subscriptions_create', params)
2753
- return _crud.require_boolean_result(result, 'user_subscriptions_create')
2794
+ return _methods.require_user_subscriptions_create_result(result)
2754
2795
 
2755
2796
  async def user_subscriptions_read(self) -> list[dict[str, object]]:
2756
- """List the current user's subscriptions.
2797
+ """List user subscriptions in server order.
2757
2798
 
2758
2799
  Returns:
2759
- User subscription dictionaries returned by the server.
2800
+ Subscription entries in server order, each with an unchanged string
2801
+ `humanUserId` and a `subscription` of `None`, a string, or a copied
2802
+ list of strings.
2760
2803
 
2761
2804
  Raises:
2762
2805
  ShotgunError: If an RPC fault or terminal transport failure occurs,
@@ -2766,7 +2809,7 @@ class AsyncShotgun:
2766
2809
  policy.
2767
2810
  """
2768
2811
  result = await self._call('user_subscriptions_read', None)
2769
- return _methods.require_dictionary_list_result(result, 'user_subscriptions_read')
2812
+ return _methods.require_user_subscriptions_result(result)
2770
2813
 
2771
2814
  async def work_schedule_read(
2772
2815
  self,