sg-python-api 0.1.1__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.
- {sg_python_api-0.1.1.dist-info → sg_python_api-0.1.2.dist-info}/METADATA +14 -14
- {sg_python_api-0.1.1.dist-info → sg_python_api-0.1.2.dist-info}/RECORD +10 -10
- shotgun_api3/_async_client.py +72 -50
- shotgun_api3/_client.py +77 -59
- shotgun_api3/_files.py +22 -102
- shotgun_api3/_methods.py +98 -5
- shotgun_api3/_protocol.py +0 -93
- shotgun_api3/_version.py +1 -1
- {sg_python_api-0.1.1.dist-info → sg_python_api-0.1.2.dist-info}/WHEEL +0 -0
- {sg_python_api-0.1.1.dist-info → sg_python_api-0.1.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sg-python-api
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
48
|
-
site before replacing its production client. The [migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
49
|
-
through the switch, and the [compatibility reference](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
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,7 +55,7 @@ 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.
|
|
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
60
|
From a checkout, install into the Python environment that runs your application:
|
|
61
61
|
|
|
@@ -130,23 +130,23 @@ operation fails. Without the `async` extra, importing `AsyncShotgun` raises `Imp
|
|
|
130
130
|
|
|
131
131
|
Share an `AsyncShotgun` only among tasks on one event loop, and cancel its work by cancelling the
|
|
132
132
|
task. A cancelled write may already have reached the server, so check its outcome before repeating
|
|
133
|
-
it. The [async reference](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
133
|
+
it. The [async reference](https://github.com/dactylo/sg-python-api/blob/v0.1.2/COMPATIBILITY.md#async-extension-surfaces) covers shutdown and
|
|
134
134
|
cancellation in detail.
|
|
135
135
|
|
|
136
136
|
## Documentation
|
|
137
137
|
|
|
138
138
|
| If you want to... | Read |
|
|
139
139
|
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
140
|
-
| Replace Autodesk's client in an existing application | [Migration guide](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
141
|
-
| Check what behaves differently from Autodesk's client | [Compatibility](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
142
|
-
| Tune pooling and page prefetch, or observe requests | [Configuration extensions](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
143
|
-
| Confirm which client loads and run a smoke test | [Testing an installation](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
144
|
-
| Compare measured latency and transfer memory | [Benchmarks](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
145
|
-
| See changes by version | [Changelog](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
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) |
|
|
146
146
|
|
|
147
147
|
## Development
|
|
148
148
|
|
|
149
|
-
See the [development guide](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
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.
|
|
150
150
|
With those tools installed, run:
|
|
151
151
|
|
|
152
152
|
```sh
|
|
@@ -159,6 +159,6 @@ make check
|
|
|
159
159
|
|
|
160
160
|
## License
|
|
161
161
|
|
|
162
|
-
sg-python-api is available under the [MIT License](https://github.com/dactylo/sg-python-api/blob/v0.1.
|
|
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
|
|
163
163
|
not affiliated with or endorsed by Autodesk. Flow Production Tracking is a product of Autodesk,
|
|
164
164
|
Inc.
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
shotgun_api3/__init__.py,sha256=fviJGOi_Gb9jHi5Q9ty5tIbeIvxMtLNrKx_4HJVgemI,2011
|
|
2
|
-
shotgun_api3/_async_client.py,sha256=
|
|
2
|
+
shotgun_api3/_async_client.py,sha256=UYVk_IGzMcUguJ4oCV-Nof4mL2kvzYYGuiYvW4gHeMs,113167
|
|
3
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=
|
|
7
|
+
shotgun_api3/_client.py,sha256=jtemxhdhFr1gwE7qB6K-62Sohb2C_S4dEO7uKpVh2Uo,102109
|
|
8
8
|
shotgun_api3/_config.py,sha256=fALxy5xGI3A0nECsRDyls_9A8tvjB0DNSpTu1agNEHw,54931
|
|
9
9
|
shotgun_api3/_crud.py,sha256=Ax36jA2E-lauRf2D-2rmRDj61SilOygtkjKgs5XDCUs,30319
|
|
10
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
|
|
14
|
-
shotgun_api3/_methods.py,sha256=
|
|
15
|
-
shotgun_api3/_protocol.py,sha256=
|
|
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=
|
|
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.
|
|
29
|
-
sg_python_api-0.1.
|
|
30
|
-
sg_python_api-0.1.
|
|
31
|
-
sg_python_api-0.1.
|
|
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,,
|
shotgun_api3/_async_client.py
CHANGED
|
@@ -59,7 +59,6 @@ from ._config import (
|
|
|
59
59
|
server_host,
|
|
60
60
|
server_page_limit,
|
|
61
61
|
)
|
|
62
|
-
from ._errors import ShotgunError
|
|
63
62
|
from ._execution import SuppressedExceptionCapture, release_lease, retire_transport
|
|
64
63
|
from ._signatures import bind_arguments, expose_signature
|
|
65
64
|
from ._transport import (
|
|
@@ -1037,29 +1036,6 @@ class AsyncShotgun:
|
|
|
1037
1036
|
lambda _text, _headers: None,
|
|
1038
1037
|
)
|
|
1039
1038
|
|
|
1040
|
-
@staticmethod
|
|
1041
|
-
async def _upload_form(
|
|
1042
|
-
driver: AsyncDriver,
|
|
1043
|
-
context: RequestContext,
|
|
1044
|
-
prepared: _files.PreparedUpload,
|
|
1045
|
-
) -> Never:
|
|
1046
|
-
"""Send the observed form request without guessing a success result."""
|
|
1047
|
-
if context.canonical_origin is None:
|
|
1048
|
-
raise ValueError('server parameters must be configured before upload I/O')
|
|
1049
|
-
exchange = _files.build_form_upload_exchange(
|
|
1050
|
-
context.canonical_origin,
|
|
1051
|
-
context.user_agent,
|
|
1052
|
-
prepared,
|
|
1053
|
-
context.auth,
|
|
1054
|
-
chunk_size=_ASYNC_FILE_BATCH_SIZE,
|
|
1055
|
-
)
|
|
1056
|
-
|
|
1057
|
-
def reject_success(_text: str, _headers: Mapping[str, str]) -> Never:
|
|
1058
|
-
raise ShotgunError('form upload success response is not established')
|
|
1059
|
-
|
|
1060
|
-
await _execute_upload_exchange(driver, context, exchange, reject_success)
|
|
1061
|
-
raise AssertionError('form upload response parser returned')
|
|
1062
|
-
|
|
1063
1039
|
@staticmethod
|
|
1064
1040
|
async def _upload_link(
|
|
1065
1041
|
driver: AsyncDriver,
|
|
@@ -1162,14 +1138,14 @@ class AsyncShotgun:
|
|
|
1162
1138
|
arguments = prepared.options.arguments
|
|
1163
1139
|
capabilities = await self._ensure_readiness(context, driver)
|
|
1164
1140
|
operation_context = await self._authenticated_context(context, driver)
|
|
1165
|
-
upload_path = _files.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
+
)
|
|
1170
1148
|
)
|
|
1171
|
-
if upload_path == 'form':
|
|
1172
|
-
return await self._upload_form(driver, operation_context, prepared)
|
|
1173
1149
|
multipart = upload_path == 'multipart'
|
|
1174
1150
|
upload_info = await self._upload_allocation(
|
|
1175
1151
|
driver,
|
|
@@ -1208,6 +1184,16 @@ class AsyncShotgun:
|
|
|
1208
1184
|
prepared: _files.PreparedUpload,
|
|
1209
1185
|
) -> int:
|
|
1210
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
|
+
)
|
|
1211
1197
|
upload_info = await self._upload_allocation(
|
|
1212
1198
|
driver,
|
|
1213
1199
|
context,
|
|
@@ -1702,6 +1688,9 @@ class AsyncShotgun:
|
|
|
1702
1688
|
async def export_page(self, *args: object, **kwargs: object) -> str:
|
|
1703
1689
|
"""Export a page using the documented arguments.
|
|
1704
1690
|
|
|
1691
|
+
The Page or its layout must be marked API Exportable in the web UI, or
|
|
1692
|
+
the server rejects the export.
|
|
1693
|
+
|
|
1705
1694
|
Args:
|
|
1706
1695
|
*args: Positional values from the documented `export_page`
|
|
1707
1696
|
signature.
|
|
@@ -2022,7 +2011,11 @@ class AsyncShotgun:
|
|
|
2022
2011
|
entity,
|
|
2023
2012
|
seed_entity_field,
|
|
2024
2013
|
)
|
|
2025
|
-
result = await self._call(
|
|
2014
|
+
result = await self._call(
|
|
2015
|
+
'nav_search',
|
|
2016
|
+
params,
|
|
2017
|
+
_InvocationOptions(logical_method_name='nav_search_entity'),
|
|
2018
|
+
)
|
|
2026
2019
|
return _methods.require_dictionary_list_result(result, 'nav_search_entity')
|
|
2027
2020
|
|
|
2028
2021
|
async def nav_search_string(
|
|
@@ -2053,7 +2046,11 @@ class AsyncShotgun:
|
|
|
2053
2046
|
search_string,
|
|
2054
2047
|
seed_entity_field,
|
|
2055
2048
|
)
|
|
2056
|
-
result = await self._call(
|
|
2049
|
+
result = await self._call(
|
|
2050
|
+
'nav_search',
|
|
2051
|
+
params,
|
|
2052
|
+
_InvocationOptions(logical_method_name='nav_search_string'),
|
|
2053
|
+
)
|
|
2057
2054
|
return _methods.require_dictionary_list_result(result, 'nav_search_string')
|
|
2058
2055
|
|
|
2059
2056
|
async def note_thread_read(
|
|
@@ -2078,7 +2075,11 @@ class AsyncShotgun:
|
|
|
2078
2075
|
policy.
|
|
2079
2076
|
"""
|
|
2080
2077
|
params = _methods.build_note_thread_read_params(note_id, entity_fields)
|
|
2081
|
-
result = await self._call(
|
|
2078
|
+
result = await self._call(
|
|
2079
|
+
'note_thread_contents',
|
|
2080
|
+
params,
|
|
2081
|
+
_InvocationOptions(logical_method_name='note_thread_read'),
|
|
2082
|
+
)
|
|
2082
2083
|
return _methods.require_dictionary_list_result(result, 'note_thread_read')
|
|
2083
2084
|
|
|
2084
2085
|
async def preferences_read(self, prefs: list[str] | None = None) -> dict[str, object]:
|
|
@@ -2357,7 +2358,8 @@ class AsyncShotgun:
|
|
|
2357
2358
|
|
|
2358
2359
|
Supply exactly one of `thumbnail_path` and `source_entity`. A local
|
|
2359
2360
|
thumbnail is uploaded to the first target before it is shared with the
|
|
2360
|
-
remaining targets.
|
|
2361
|
+
remaining targets. A local thumbnail is rejected before any upload
|
|
2362
|
+
request if the server requires legacy form uploads.
|
|
2361
2363
|
|
|
2362
2364
|
Args:
|
|
2363
2365
|
entities: Nonempty list of target entities with `type` and `id`.
|
|
@@ -2374,7 +2376,8 @@ class AsyncShotgun:
|
|
|
2374
2376
|
TypeError: If forwarded arguments do not match the upload signature
|
|
2375
2377
|
or include `field_name`.
|
|
2376
2378
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2377
|
-
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.
|
|
2378
2381
|
ShotgunThumbnailNotReady: If an uploaded source stays transient.
|
|
2379
2382
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2380
2383
|
ValueError: If authenticated HTTP violates the configured origin
|
|
@@ -2647,9 +2650,8 @@ class AsyncShotgun:
|
|
|
2647
2650
|
async def upload(self, *args: object, **kwargs: object) -> int:
|
|
2648
2651
|
"""Upload a file or thumbnail and return its Attachment ID.
|
|
2649
2652
|
|
|
2650
|
-
A
|
|
2651
|
-
|
|
2652
|
-
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.
|
|
2653
2655
|
|
|
2654
2656
|
Args:
|
|
2655
2657
|
*args: Positional arguments from the documented upload signature.
|
|
@@ -2661,8 +2663,8 @@ class AsyncShotgun:
|
|
|
2661
2663
|
Raises:
|
|
2662
2664
|
TypeError: If arguments do not match the documented signature.
|
|
2663
2665
|
ShotgunError: If the file or an upload response is invalid, an RPC
|
|
2664
|
-
fault or terminal transport failure occurs, or the
|
|
2665
|
-
form
|
|
2666
|
+
fault or terminal transport failure occurs, or the server
|
|
2667
|
+
requires legacy form uploads.
|
|
2666
2668
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2667
2669
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2668
2670
|
policy.
|
|
@@ -2694,6 +2696,9 @@ class AsyncShotgun:
|
|
|
2694
2696
|
) -> int:
|
|
2695
2697
|
"""Upload a filmstrip thumbnail through `upload`.
|
|
2696
2698
|
|
|
2699
|
+
Servers requiring legacy form uploads are rejected before any upload
|
|
2700
|
+
request is sent.
|
|
2701
|
+
|
|
2697
2702
|
Args:
|
|
2698
2703
|
entity_type: Entity type receiving the thumbnail.
|
|
2699
2704
|
entity_id: Numeric identifier of the receiving entity.
|
|
@@ -2706,7 +2711,8 @@ class AsyncShotgun:
|
|
|
2706
2711
|
Raises:
|
|
2707
2712
|
TypeError: If forwarded arguments do not match `upload`.
|
|
2708
2713
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2709
|
-
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.
|
|
2710
2716
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2711
2717
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2712
2718
|
policy.
|
|
@@ -2728,6 +2734,9 @@ class AsyncShotgun:
|
|
|
2728
2734
|
) -> int:
|
|
2729
2735
|
"""Upload a static thumbnail through `upload`.
|
|
2730
2736
|
|
|
2737
|
+
Servers requiring legacy form uploads are rejected before any upload
|
|
2738
|
+
request is sent.
|
|
2739
|
+
|
|
2731
2740
|
Args:
|
|
2732
2741
|
entity_type: Entity type receiving the thumbnail.
|
|
2733
2742
|
entity_id: Numeric identifier of the receiving entity.
|
|
@@ -2740,7 +2749,8 @@ class AsyncShotgun:
|
|
|
2740
2749
|
Raises:
|
|
2741
2750
|
TypeError: If forwarded arguments do not match `upload`.
|
|
2742
2751
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2743
|
-
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.
|
|
2744
2754
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2745
2755
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2746
2756
|
policy.
|
|
@@ -2756,28 +2766,40 @@ class AsyncShotgun:
|
|
|
2756
2766
|
async def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
|
|
2757
2767
|
"""Assign subscriptions to users.
|
|
2758
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
|
+
|
|
2759
2773
|
Args:
|
|
2760
|
-
users: Subscription dictionaries
|
|
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.
|
|
2761
2779
|
|
|
2762
2780
|
Returns:
|
|
2763
|
-
|
|
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.
|
|
2764
2783
|
|
|
2765
2784
|
Raises:
|
|
2766
2785
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2767
|
-
or if
|
|
2786
|
+
or if `users` contains invalid or duplicate entries, or the
|
|
2787
|
+
server result is not a mapping with string keys.
|
|
2768
2788
|
ProtocolError: If a server request returns an HTTP error.
|
|
2769
2789
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2770
2790
|
policy.
|
|
2771
2791
|
"""
|
|
2772
2792
|
params = _methods.build_user_subscriptions_create_params(users)
|
|
2773
2793
|
result = await self._call('user_subscriptions_create', params)
|
|
2774
|
-
return
|
|
2794
|
+
return _methods.require_user_subscriptions_create_result(result)
|
|
2775
2795
|
|
|
2776
2796
|
async def user_subscriptions_read(self) -> list[dict[str, object]]:
|
|
2777
|
-
"""List
|
|
2797
|
+
"""List user subscriptions in server order.
|
|
2778
2798
|
|
|
2779
2799
|
Returns:
|
|
2780
|
-
|
|
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.
|
|
2781
2803
|
|
|
2782
2804
|
Raises:
|
|
2783
2805
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
@@ -2787,7 +2809,7 @@ class AsyncShotgun:
|
|
|
2787
2809
|
policy.
|
|
2788
2810
|
"""
|
|
2789
2811
|
result = await self._call('user_subscriptions_read', None)
|
|
2790
|
-
return _methods.
|
|
2812
|
+
return _methods.require_user_subscriptions_result(result)
|
|
2791
2813
|
|
|
2792
2814
|
async def work_schedule_read(
|
|
2793
2815
|
self,
|
shotgun_api3/_client.py
CHANGED
|
@@ -136,10 +136,8 @@ _CONSTRUCTOR_PARAMETERS = (
|
|
|
136
136
|
)
|
|
137
137
|
_UPLOAD_DOCSTRING = """Upload a file or thumbnail and return its Attachment ID.
|
|
138
138
|
|
|
139
|
-
A
|
|
140
|
-
|
|
141
|
-
route. The form request has already been sent when this error is
|
|
142
|
-
raised.
|
|
139
|
+
A server requiring legacy form uploads raises `ShotgunError` before
|
|
140
|
+
any upload request is sent. Server capability reads may occur.
|
|
143
141
|
|
|
144
142
|
Args:
|
|
145
143
|
entity_type: Entity type receiving the upload.
|
|
@@ -156,8 +154,8 @@ Returns:
|
|
|
156
154
|
Raises:
|
|
157
155
|
TypeError: If arguments do not match the documented signature.
|
|
158
156
|
ShotgunError: If the file or an upload response is invalid, an RPC
|
|
159
|
-
fault or terminal transport failure occurs, or the
|
|
160
|
-
form
|
|
157
|
+
fault or terminal transport failure occurs, or the server
|
|
158
|
+
requires legacy form uploads.
|
|
161
159
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
162
160
|
ValueError: If authenticated HTTP violates the configured origin policy.
|
|
163
161
|
"""
|
|
@@ -881,28 +879,6 @@ class Shotgun:
|
|
|
881
879
|
lambda _text, _headers: None,
|
|
882
880
|
)
|
|
883
881
|
|
|
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
882
|
@staticmethod
|
|
907
883
|
def _upload_link(
|
|
908
884
|
driver: Driver,
|
|
@@ -999,14 +975,14 @@ class Shotgun:
|
|
|
999
975
|
"""Execute one prepared upload through a captured driver lease."""
|
|
1000
976
|
arguments = prepared.options.arguments
|
|
1001
977
|
capabilities = self._ensure_readiness(context, driver)
|
|
1002
|
-
upload_path = _files.
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
978
|
+
upload_path = _files.require_supported_upload_path(
|
|
979
|
+
_files.choose_upload_path(
|
|
980
|
+
capabilities.server_info,
|
|
981
|
+
arguments.entity_type,
|
|
982
|
+
arguments.field_name,
|
|
983
|
+
prepared.file_size,
|
|
984
|
+
)
|
|
1007
985
|
)
|
|
1008
|
-
if upload_path == 'form':
|
|
1009
|
-
return self._upload_form(driver, context, prepared)
|
|
1010
986
|
multipart = upload_path == 'multipart'
|
|
1011
987
|
upload_info = self._upload_allocation(driver, context, prepared, multipart)
|
|
1012
988
|
if multipart:
|
|
@@ -1030,7 +1006,16 @@ class Shotgun:
|
|
|
1030
1006
|
prepared: _files.PreparedUpload,
|
|
1031
1007
|
) -> int:
|
|
1032
1008
|
"""Execute the exact direct-storage thumbnail workflow."""
|
|
1033
|
-
self._ensure_readiness(context, driver)
|
|
1009
|
+
capabilities = self._ensure_readiness(context, driver)
|
|
1010
|
+
arguments = prepared.options.arguments
|
|
1011
|
+
_files.require_supported_upload_path(
|
|
1012
|
+
_files.choose_upload_path(
|
|
1013
|
+
capabilities.server_info,
|
|
1014
|
+
arguments.entity_type,
|
|
1015
|
+
arguments.field_name,
|
|
1016
|
+
prepared.file_size,
|
|
1017
|
+
)
|
|
1018
|
+
)
|
|
1034
1019
|
upload_info = self._upload_allocation(driver, context, prepared, multipart=False)
|
|
1035
1020
|
self._upload_direct(driver, context, prepared, upload_info)
|
|
1036
1021
|
return self._upload_link(driver, context, prepared, upload_info)
|
|
@@ -1480,6 +1465,9 @@ class Shotgun:
|
|
|
1480
1465
|
def export_page(self, *args: object, **kwargs: object) -> str:
|
|
1481
1466
|
"""Export a page using the documented arguments.
|
|
1482
1467
|
|
|
1468
|
+
The Page or its layout must be marked API Exportable in the web UI, or
|
|
1469
|
+
the server rejects the export.
|
|
1470
|
+
|
|
1483
1471
|
Args:
|
|
1484
1472
|
*args: Positional values from the documented `export_page`
|
|
1485
1473
|
signature.
|
|
@@ -1827,7 +1815,11 @@ class Shotgun:
|
|
|
1827
1815
|
seed_entity_field,
|
|
1828
1816
|
)
|
|
1829
1817
|
return _methods.require_dictionary_list_result(
|
|
1830
|
-
self._call(
|
|
1818
|
+
self._call(
|
|
1819
|
+
'nav_search',
|
|
1820
|
+
params,
|
|
1821
|
+
invocation_options=_InvocationOptions(logical_method_name='nav_search_entity'),
|
|
1822
|
+
),
|
|
1831
1823
|
'nav_search_entity',
|
|
1832
1824
|
)
|
|
1833
1825
|
|
|
@@ -1860,7 +1852,11 @@ class Shotgun:
|
|
|
1860
1852
|
seed_entity_field,
|
|
1861
1853
|
)
|
|
1862
1854
|
return _methods.require_dictionary_list_result(
|
|
1863
|
-
self._call(
|
|
1855
|
+
self._call(
|
|
1856
|
+
'nav_search',
|
|
1857
|
+
params,
|
|
1858
|
+
invocation_options=_InvocationOptions(logical_method_name='nav_search_string'),
|
|
1859
|
+
),
|
|
1864
1860
|
'nav_search_string',
|
|
1865
1861
|
)
|
|
1866
1862
|
|
|
@@ -1887,7 +1883,11 @@ class Shotgun:
|
|
|
1887
1883
|
"""
|
|
1888
1884
|
params = _methods.build_note_thread_read_params(note_id, entity_fields)
|
|
1889
1885
|
return _methods.require_dictionary_list_result(
|
|
1890
|
-
self._call(
|
|
1886
|
+
self._call(
|
|
1887
|
+
'note_thread_contents',
|
|
1888
|
+
params,
|
|
1889
|
+
invocation_options=_InvocationOptions(logical_method_name='note_thread_read'),
|
|
1890
|
+
),
|
|
1891
1891
|
'note_thread_read',
|
|
1892
1892
|
)
|
|
1893
1893
|
|
|
@@ -2185,7 +2185,8 @@ class Shotgun:
|
|
|
2185
2185
|
|
|
2186
2186
|
Supply exactly one of `thumbnail_path` and `source_entity`. A local
|
|
2187
2187
|
thumbnail is uploaded to the first target before it is shared with the
|
|
2188
|
-
remaining targets.
|
|
2188
|
+
remaining targets. A local thumbnail is rejected before any upload
|
|
2189
|
+
request if the server requires legacy form uploads.
|
|
2189
2190
|
|
|
2190
2191
|
Args:
|
|
2191
2192
|
entities: Nonempty list of target entities with `type` and `id`.
|
|
@@ -2202,7 +2203,8 @@ class Shotgun:
|
|
|
2202
2203
|
TypeError: If forwarded arguments do not match the upload signature
|
|
2203
2204
|
or include `field_name`.
|
|
2204
2205
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2205
|
-
or if the source selection, entities, or transport fails
|
|
2206
|
+
or if the source selection, entities, or transport fails, or a
|
|
2207
|
+
local upload requires the legacy form route.
|
|
2206
2208
|
ShotgunThumbnailNotReady: If an uploaded source stays transient.
|
|
2207
2209
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2208
2210
|
ValueError: If authenticated HTTP violates the configured origin
|
|
@@ -2468,9 +2470,8 @@ class Shotgun:
|
|
|
2468
2470
|
def upload(self, *args: object, **kwargs: object) -> int:
|
|
2469
2471
|
"""Upload a file or thumbnail and return its Attachment ID.
|
|
2470
2472
|
|
|
2471
|
-
A
|
|
2472
|
-
|
|
2473
|
-
The form request has already been sent when this error is raised.
|
|
2473
|
+
A server requiring legacy form uploads raises `ShotgunError` before any
|
|
2474
|
+
upload request is sent. Server capability reads may occur.
|
|
2474
2475
|
|
|
2475
2476
|
Args:
|
|
2476
2477
|
*args: Positional arguments from the documented upload signature;
|
|
@@ -2483,8 +2484,8 @@ class Shotgun:
|
|
|
2483
2484
|
Raises:
|
|
2484
2485
|
TypeError: If arguments do not match the documented signature.
|
|
2485
2486
|
ShotgunError: If the file or an upload response is invalid, an RPC
|
|
2486
|
-
fault or terminal transport failure occurs, or the
|
|
2487
|
-
form
|
|
2487
|
+
fault or terminal transport failure occurs, or the server
|
|
2488
|
+
requires legacy form uploads.
|
|
2488
2489
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2489
2490
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2490
2491
|
policy.
|
|
@@ -2516,6 +2517,9 @@ class Shotgun:
|
|
|
2516
2517
|
) -> int:
|
|
2517
2518
|
"""Upload a filmstrip thumbnail through `upload`.
|
|
2518
2519
|
|
|
2520
|
+
Servers requiring legacy form uploads are rejected before any upload
|
|
2521
|
+
request is sent.
|
|
2522
|
+
|
|
2519
2523
|
Args:
|
|
2520
2524
|
entity_type: Entity type receiving the thumbnail.
|
|
2521
2525
|
entity_id: Numeric identifier of the receiving entity.
|
|
@@ -2528,7 +2532,8 @@ class Shotgun:
|
|
|
2528
2532
|
Raises:
|
|
2529
2533
|
TypeError: If forwarded arguments do not match `upload`.
|
|
2530
2534
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2531
|
-
or if the file, upload response, or transport fails
|
|
2535
|
+
or if the file, upload response, or transport fails, or the
|
|
2536
|
+
server requires legacy form uploads.
|
|
2532
2537
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2533
2538
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2534
2539
|
policy.
|
|
@@ -2550,6 +2555,9 @@ class Shotgun:
|
|
|
2550
2555
|
) -> int:
|
|
2551
2556
|
"""Upload a static thumbnail through `upload`.
|
|
2552
2557
|
|
|
2558
|
+
Servers requiring legacy form uploads are rejected before any upload
|
|
2559
|
+
request is sent.
|
|
2560
|
+
|
|
2553
2561
|
Args:
|
|
2554
2562
|
entity_type: Entity type receiving the thumbnail.
|
|
2555
2563
|
entity_id: Numeric identifier of the receiving entity.
|
|
@@ -2562,7 +2570,8 @@ class Shotgun:
|
|
|
2562
2570
|
Raises:
|
|
2563
2571
|
TypeError: If forwarded arguments do not match `upload`.
|
|
2564
2572
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2565
|
-
or if the file, upload response, or transport fails
|
|
2573
|
+
or if the file, upload response, or transport fails, or the
|
|
2574
|
+
server requires legacy form uploads.
|
|
2566
2575
|
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2567
2576
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2568
2577
|
policy.
|
|
@@ -2578,44 +2587,53 @@ class Shotgun:
|
|
|
2578
2587
|
def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
|
|
2579
2588
|
"""Assign subscriptions to users.
|
|
2580
2589
|
|
|
2590
|
+
The server accepts this call from scripts and from human users with
|
|
2591
|
+
permission to manage subscriptions, such as members of the Admin
|
|
2592
|
+
permission group. It refuses other human users.
|
|
2593
|
+
|
|
2581
2594
|
Args:
|
|
2582
|
-
users: Subscription dictionaries
|
|
2595
|
+
users: Subscription dictionaries with a string or integer
|
|
2596
|
+
`humanUserId`; integers become decimal string keys and booleans
|
|
2597
|
+
are not accepted. Each user may appear only once.
|
|
2598
|
+
`subscription` is `None`, a string, or a list of strings.
|
|
2599
|
+
Lists are copied in order. Other entry keys are ignored.
|
|
2583
2600
|
|
|
2584
2601
|
Returns:
|
|
2585
|
-
|
|
2602
|
+
`True` when every per-user report is `None`, meaning the server
|
|
2603
|
+
reported no problem; `False` when any user has a non-`None` report.
|
|
2586
2604
|
|
|
2587
2605
|
Raises:
|
|
2588
2606
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2589
|
-
or if `users`
|
|
2590
|
-
|
|
2607
|
+
or if `users` contains invalid or duplicate entries, or the
|
|
2608
|
+
server result is not a mapping with string keys.
|
|
2591
2609
|
ProtocolError: If a server request returns an HTTP error.
|
|
2592
2610
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2593
2611
|
policy.
|
|
2594
2612
|
"""
|
|
2595
|
-
return
|
|
2613
|
+
return _methods.require_user_subscriptions_create_result(
|
|
2596
2614
|
self._call(
|
|
2597
2615
|
'user_subscriptions_create',
|
|
2598
2616
|
_methods.build_user_subscriptions_create_params(users),
|
|
2599
2617
|
),
|
|
2600
|
-
'user_subscriptions_create',
|
|
2601
2618
|
)
|
|
2602
2619
|
|
|
2603
2620
|
def user_subscriptions_read(self) -> list[dict[str, object]]:
|
|
2604
|
-
"""List
|
|
2621
|
+
"""List user subscriptions in server order.
|
|
2605
2622
|
|
|
2606
2623
|
Returns:
|
|
2607
|
-
|
|
2624
|
+
Subscription entries in server order, each with an unchanged string
|
|
2625
|
+
`humanUserId` and a `subscription` of `None`, a string, or a copied
|
|
2626
|
+
list of strings.
|
|
2608
2627
|
|
|
2609
2628
|
Raises:
|
|
2610
2629
|
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2611
|
-
or if the server result is
|
|
2630
|
+
or if the server result is invalid.
|
|
2612
2631
|
ProtocolError: If a server request returns an HTTP error.
|
|
2613
2632
|
ValueError: If authenticated HTTP violates the configured origin
|
|
2614
2633
|
policy.
|
|
2615
2634
|
"""
|
|
2616
|
-
return _methods.
|
|
2635
|
+
return _methods.require_user_subscriptions_result(
|
|
2617
2636
|
self._call('user_subscriptions_read', None),
|
|
2618
|
-
'user_subscriptions_read',
|
|
2619
2637
|
)
|
|
2620
2638
|
|
|
2621
2639
|
def work_schedule_read(
|
shotgun_api3/_files.py
CHANGED
|
@@ -10,7 +10,7 @@ import urllib.parse
|
|
|
10
10
|
import urllib.request
|
|
11
11
|
import uuid
|
|
12
12
|
from collections.abc import Callable, Generator, Iterator, Mapping, Sequence
|
|
13
|
-
from contextlib import
|
|
13
|
+
from contextlib import contextmanager
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import BinaryIO, Literal, TypeAlias, TypedDict, cast
|
|
16
16
|
|
|
@@ -35,7 +35,6 @@ _MULTIPART_CHUNK_SIZE = 20_000_000
|
|
|
35
35
|
_Origin: TypeAlias = tuple[str, str, int]
|
|
36
36
|
_PART_LINK_FIELD_COUNT = 2
|
|
37
37
|
_THUMBNAIL_UPLOAD_FIELDS = ('image', 'filmstrip_image')
|
|
38
|
-
_UPLOAD_FILE = '/upload/upload_file'
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -246,17 +245,6 @@ class _ExactOriginCookiePolicy(http.cookiejar.DefaultCookiePolicy):
|
|
|
246
245
|
)
|
|
247
246
|
|
|
248
247
|
|
|
249
|
-
def _build_form_upload_fields(prepared: PreparedUpload) -> dict[str, str]:
|
|
250
|
-
"""Build the non-authenticated legacy form-upload fields."""
|
|
251
|
-
arguments = prepared.options.arguments
|
|
252
|
-
return {
|
|
253
|
-
'entity_type': arguments.entity_type,
|
|
254
|
-
'entity_id': str(arguments.entity_id),
|
|
255
|
-
'field_name': arguments.field_name or '',
|
|
256
|
-
'display_name': arguments.display_name or prepared.filename,
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
248
|
def _entity_reference(entity: Mapping[str, object]) -> str:
|
|
261
249
|
"""Serialize one entity as the upload endpoint reference grammar."""
|
|
262
250
|
entity_type = entity.get('type')
|
|
@@ -295,48 +283,6 @@ def _iter_file(
|
|
|
295
283
|
remaining -= len(chunk)
|
|
296
284
|
|
|
297
285
|
|
|
298
|
-
def _multipart_content_length(
|
|
299
|
-
plan: _protocol.MultipartPlan,
|
|
300
|
-
file_sizes: Mapping[str, int],
|
|
301
|
-
) -> int:
|
|
302
|
-
"""Return the exact byte length of a multipart plan."""
|
|
303
|
-
return sum(
|
|
304
|
-
file_sizes[segment.name] if isinstance(segment, _protocol.FileSlot) else len(segment)
|
|
305
|
-
for segment in plan.segments
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _pump_multipart(
|
|
310
|
-
plan: _protocol.MultipartPlan,
|
|
311
|
-
opened: Mapping[str, BinaryIO],
|
|
312
|
-
chunk_size: int = 65536,
|
|
313
|
-
) -> Generator[bytes, None, None]:
|
|
314
|
-
"""Yield multipart framing while reading file slots incrementally."""
|
|
315
|
-
if chunk_size <= 0:
|
|
316
|
-
raise ValueError('multipart chunk size must be positive')
|
|
317
|
-
for segment in plan.segments:
|
|
318
|
-
if isinstance(segment, _protocol.FileSlot):
|
|
319
|
-
handle = opened[segment.name]
|
|
320
|
-
while chunk := handle.read(chunk_size):
|
|
321
|
-
yield chunk
|
|
322
|
-
else:
|
|
323
|
-
yield segment
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def _pump_multipart_from_disk(
|
|
327
|
-
plan: _protocol.MultipartPlan,
|
|
328
|
-
paths: Mapping[str, str],
|
|
329
|
-
chunk_size: int = 65536,
|
|
330
|
-
) -> Generator[bytes, None, None]:
|
|
331
|
-
"""Pump multipart framing through lazily opened disk files."""
|
|
332
|
-
with ExitStack() as stack:
|
|
333
|
-
opened: dict[str, BinaryIO] = {}
|
|
334
|
-
for segment in plan.segments:
|
|
335
|
-
if isinstance(segment, _protocol.FileSlot) and segment.name not in opened:
|
|
336
|
-
opened[segment.name] = stack.enter_context(Path(paths[segment.name]).open('rb'))
|
|
337
|
-
yield from _pump_multipart(plan, opened, chunk_size)
|
|
338
|
-
|
|
339
|
-
|
|
340
286
|
def _requires_direct_upload(
|
|
341
287
|
server_info: Mapping[str, object],
|
|
342
288
|
entity_type: str,
|
|
@@ -694,53 +640,6 @@ def build_download_redirect(
|
|
|
694
640
|
return build_download_request(context, location, {'user-agent': context.user_agent})
|
|
695
641
|
|
|
696
642
|
|
|
697
|
-
def build_form_upload_exchange(
|
|
698
|
-
canonical_origin: str,
|
|
699
|
-
user_agent: str,
|
|
700
|
-
prepared: PreparedUpload,
|
|
701
|
-
auth: Mapping[str, object],
|
|
702
|
-
chunk_size: int = 65536,
|
|
703
|
-
) -> UploadExchange:
|
|
704
|
-
"""Plan one restartable legacy multipart form upload.
|
|
705
|
-
|
|
706
|
-
The body can be rebuilt, but the operation is not replay-safe after
|
|
707
|
-
transmission.
|
|
708
|
-
|
|
709
|
-
Args:
|
|
710
|
-
canonical_origin: Captured site origin.
|
|
711
|
-
user_agent: Captured user-agent header value.
|
|
712
|
-
prepared: Validated upload metadata and arguments.
|
|
713
|
-
auth: Captured authentication form fields.
|
|
714
|
-
chunk_size: Maximum file bytes read per iteration.
|
|
715
|
-
|
|
716
|
-
Returns:
|
|
717
|
-
Complete restartable form-upload exchange.
|
|
718
|
-
"""
|
|
719
|
-
fields = add_auth_fields(_build_form_upload_fields(prepared), auth)
|
|
720
|
-
slot = _protocol.FileSlot(
|
|
721
|
-
name='file',
|
|
722
|
-
filename=prepared.filename,
|
|
723
|
-
content_type='application/octet-stream',
|
|
724
|
-
)
|
|
725
|
-
plan = _protocol.build_multipart_plan(fields, [slot])
|
|
726
|
-
path = prepared.options.arguments.path
|
|
727
|
-
return UploadExchange(
|
|
728
|
-
body_factory=lambda: _pump_multipart_from_disk(
|
|
729
|
-
plan,
|
|
730
|
-
{'file': path},
|
|
731
|
-
chunk_size,
|
|
732
|
-
),
|
|
733
|
-
headers={
|
|
734
|
-
'content-length': str(_multipart_content_length(plan, {'file': prepared.file_size})),
|
|
735
|
-
'content-type': plan.content_type,
|
|
736
|
-
'user-agent': user_agent,
|
|
737
|
-
},
|
|
738
|
-
method='POST',
|
|
739
|
-
operation=SHARE_THUMBNAIL_OPERATION,
|
|
740
|
-
url=f'{canonical_origin}{_UPLOAD_FILE}',
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
|
|
744
643
|
def build_upload_endpoint_exchange(
|
|
745
644
|
canonical_origin: str | None,
|
|
746
645
|
user_agent: str,
|
|
@@ -1018,6 +917,27 @@ def prepare_upload(options: UploadOptions) -> PreparedUpload:
|
|
|
1018
917
|
return PreparedUpload(filename=path.name, file_size=file_size, options=options)
|
|
1019
918
|
|
|
1020
919
|
|
|
920
|
+
def require_supported_upload_path(
|
|
921
|
+
upload_path: Literal['form', 'direct', 'multipart'],
|
|
922
|
+
) -> Literal['direct', 'multipart']:
|
|
923
|
+
"""Reject the unsupported legacy form route before upload I/O.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
upload_path: Route selected from server capabilities.
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
The selected supported route.
|
|
930
|
+
|
|
931
|
+
Raises:
|
|
932
|
+
ShotgunError: If the server requires legacy form uploads.
|
|
933
|
+
"""
|
|
934
|
+
if upload_path == 'form':
|
|
935
|
+
raise ShotgunError(
|
|
936
|
+
'server requires legacy form uploads, which this client does not support'
|
|
937
|
+
)
|
|
938
|
+
return upload_path
|
|
939
|
+
|
|
940
|
+
|
|
1021
941
|
def resolve_attachment_url(attachment: object, base_url: str) -> str | None:
|
|
1022
942
|
"""Resolve a supported attachment reference to its download URL.
|
|
1023
943
|
|
shotgun_api3/_methods.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import datetime
|
|
5
5
|
import inspect
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
7
|
from typing import Any, NotRequired, TypedDict, cast
|
|
8
8
|
|
|
9
9
|
from . import _crud, _protocol
|
|
@@ -64,6 +64,9 @@ _ACTIVITY_STREAM_READ_SIGNATURE = inspect.Signature(
|
|
|
64
64
|
)
|
|
65
65
|
_EXPORT_PAGE_DOCSTRING = """Export a page using the documented arguments.
|
|
66
66
|
|
|
67
|
+
The Page or its layout must be marked API Exportable in the web UI,
|
|
68
|
+
or the server rejects the export.
|
|
69
|
+
|
|
67
70
|
Args:
|
|
68
71
|
page_id: Numeric page identifier.
|
|
69
72
|
format: Required nonempty export format name.
|
|
@@ -328,6 +331,18 @@ def _entity(value: object) -> object:
|
|
|
328
331
|
return _protocol.optimize_entity(value)
|
|
329
332
|
|
|
330
333
|
|
|
334
|
+
def _is_subscription_value(value: object) -> bool:
|
|
335
|
+
"""Check the supported subscription value forms."""
|
|
336
|
+
return (
|
|
337
|
+
value is None
|
|
338
|
+
or isinstance(value, str)
|
|
339
|
+
or (
|
|
340
|
+
isinstance(value, list)
|
|
341
|
+
and all(isinstance(item, str) for item in cast('list[object]', value))
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
331
346
|
def _project_entity(value: object) -> object:
|
|
332
347
|
"""Optimize an optional project entity."""
|
|
333
348
|
return None if value is None else _entity(value)
|
|
@@ -920,11 +935,38 @@ def build_user_subscriptions_create_params(users: object) -> dict[str, object]:
|
|
|
920
935
|
users,
|
|
921
936
|
'user_subscriptions_create users must be a list',
|
|
922
937
|
)
|
|
923
|
-
|
|
938
|
+
entry_message = (
|
|
939
|
+
'user_subscriptions_create users must contain dictionaries with '
|
|
940
|
+
'humanUserId and subscription keys'
|
|
941
|
+
)
|
|
942
|
+
params: dict[str, object] = {}
|
|
924
943
|
for member in validated:
|
|
925
|
-
if not isinstance(member, dict)
|
|
926
|
-
raise ShotgunError(
|
|
927
|
-
|
|
944
|
+
if not isinstance(member, dict):
|
|
945
|
+
raise ShotgunError(entry_message)
|
|
946
|
+
entry = cast('dict[str, object]', member)
|
|
947
|
+
if 'humanUserId' not in entry or 'subscription' not in entry:
|
|
948
|
+
raise ShotgunError(entry_message)
|
|
949
|
+
user_id = entry['humanUserId']
|
|
950
|
+
if isinstance(user_id, bool) or not isinstance(user_id, (str, int)):
|
|
951
|
+
raise ShotgunError(
|
|
952
|
+
'user_subscriptions_create users must contain string or integer humanUserId values'
|
|
953
|
+
)
|
|
954
|
+
if not _is_subscription_value(entry['subscription']):
|
|
955
|
+
raise ShotgunError(
|
|
956
|
+
'user_subscriptions_create users must contain valid subscription values'
|
|
957
|
+
)
|
|
958
|
+
key = str(user_id)
|
|
959
|
+
if key in params:
|
|
960
|
+
raise ShotgunError(
|
|
961
|
+
'user_subscriptions_create users must not contain duplicate user IDs'
|
|
962
|
+
)
|
|
963
|
+
subscription = entry['subscription']
|
|
964
|
+
params[key] = (
|
|
965
|
+
cast('list[str]', subscription).copy()
|
|
966
|
+
if isinstance(subscription, list)
|
|
967
|
+
else subscription
|
|
968
|
+
)
|
|
969
|
+
return params
|
|
928
970
|
|
|
929
971
|
|
|
930
972
|
def build_work_schedule_read_params(
|
|
@@ -1124,6 +1166,57 @@ def require_string_result(value: object, method_name: str) -> str:
|
|
|
1124
1166
|
return value
|
|
1125
1167
|
|
|
1126
1168
|
|
|
1169
|
+
def require_user_subscriptions_create_result(value: object) -> bool:
|
|
1170
|
+
"""Interpret a mapping of per-user subscription assignment reports.
|
|
1171
|
+
|
|
1172
|
+
Args:
|
|
1173
|
+
value: Raw result returned by the server.
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
`True` if every report is `None`, otherwise `False`.
|
|
1177
|
+
|
|
1178
|
+
Raises:
|
|
1179
|
+
ShotgunError: If the result is not a mapping with string keys.
|
|
1180
|
+
"""
|
|
1181
|
+
message = 'user_subscriptions_create returned an invalid result mapping'
|
|
1182
|
+
if not isinstance(value, Mapping):
|
|
1183
|
+
raise ShotgunError(message)
|
|
1184
|
+
reports = cast('Mapping[object, object]', value)
|
|
1185
|
+
if any(not isinstance(key, str) for key in reports):
|
|
1186
|
+
raise ShotgunError(message)
|
|
1187
|
+
return all(report is None for report in reports.values())
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def require_user_subscriptions_result(value: object) -> list[dict[str, object]]:
|
|
1191
|
+
"""Convert a subscription mapping to ordered public entries.
|
|
1192
|
+
|
|
1193
|
+
Args:
|
|
1194
|
+
value: Raw subscription mapping returned by the server.
|
|
1195
|
+
|
|
1196
|
+
Returns:
|
|
1197
|
+
Entries with unchanged string `humanUserId` and `subscription` values
|
|
1198
|
+
of `None`, string, or list of strings. Lists are copied.
|
|
1199
|
+
|
|
1200
|
+
Raises:
|
|
1201
|
+
ShotgunError: If the result is not a valid subscription mapping.
|
|
1202
|
+
"""
|
|
1203
|
+
if not isinstance(value, Mapping):
|
|
1204
|
+
raise ShotgunError('user_subscriptions_read did not return a dictionary')
|
|
1205
|
+
subscriptions: list[dict[str, object]] = []
|
|
1206
|
+
for user_id, subscription in cast('Mapping[object, object]', value).items():
|
|
1207
|
+
if not isinstance(user_id, str) or not user_id:
|
|
1208
|
+
raise ShotgunError('user_subscriptions_read returned an invalid user ID')
|
|
1209
|
+
if not _is_subscription_value(subscription):
|
|
1210
|
+
raise ShotgunError('user_subscriptions_read returned an invalid subscription')
|
|
1211
|
+
public_subscription = (
|
|
1212
|
+
cast('list[str]', subscription).copy()
|
|
1213
|
+
if isinstance(subscription, list)
|
|
1214
|
+
else subscription
|
|
1215
|
+
)
|
|
1216
|
+
subscriptions.append({'humanUserId': user_id, 'subscription': public_subscription})
|
|
1217
|
+
return subscriptions
|
|
1218
|
+
|
|
1219
|
+
|
|
1127
1220
|
def schema_alias_error(entity_type: object) -> ShotgunError:
|
|
1128
1221
|
"""Build the error raised by the deprecated `schema` alias.
|
|
1129
1222
|
|
shotgun_api3/_protocol.py
CHANGED
|
@@ -7,7 +7,6 @@ import json
|
|
|
7
7
|
import math
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
|
-
import uuid as _uuid
|
|
11
10
|
from collections.abc import Callable, Mapping, Sequence
|
|
12
11
|
from datetime import UTC
|
|
13
12
|
from email.utils import parsedate_to_datetime
|
|
@@ -107,7 +106,6 @@ _IMF_FIXDATE_PATTERN = re.compile(
|
|
|
107
106
|
_JITTER_BYTES = 8
|
|
108
107
|
_JITTER_RANGE = 1 << (_JITTER_BYTES * 8)
|
|
109
108
|
_MIN_CONDITION_PARTS = 2
|
|
110
|
-
_MULTIPART_BOUNDARY_PATTERN = re.compile(r"[A-Za-z0-9'+._-]{1,70}")
|
|
111
109
|
_OPTIMIZABLE_RELATIONS = frozenset({'is', 'is_not', 'in', 'not_in'})
|
|
112
110
|
_RETRYABLE_STATUSES = frozenset({502, 503, 504})
|
|
113
111
|
_RETRY_AFTER_TOO_LARGE: Final = 'too-large'
|
|
@@ -209,34 +207,6 @@ class Failure:
|
|
|
209
207
|
retry_after: _RetryAfter
|
|
210
208
|
|
|
211
209
|
|
|
212
|
-
@dataclasses.dataclass(frozen=True)
|
|
213
|
-
class FileSlot:
|
|
214
|
-
"""Placeholder for file bytes inside a multipart plan.
|
|
215
|
-
|
|
216
|
-
Attributes:
|
|
217
|
-
name: Multipart form field name.
|
|
218
|
-
filename: Filename exposed in the multipart part.
|
|
219
|
-
content_type: Media type exposed in the multipart part.
|
|
220
|
-
"""
|
|
221
|
-
|
|
222
|
-
name: str
|
|
223
|
-
filename: str
|
|
224
|
-
content_type: str
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@dataclasses.dataclass(frozen=True)
|
|
228
|
-
class MultipartPlan:
|
|
229
|
-
"""Byte segments and file slots forming a multipart/form-data body.
|
|
230
|
-
|
|
231
|
-
Attributes:
|
|
232
|
-
content_type: Multipart content type including its boundary.
|
|
233
|
-
segments: Ordered encoded segments and file placeholders.
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
content_type: str
|
|
237
|
-
segments: list[bytes | FileSlot]
|
|
238
|
-
|
|
239
|
-
|
|
240
210
|
@dataclasses.dataclass(frozen=True)
|
|
241
211
|
class OperationDescriptor:
|
|
242
212
|
"""Describe the replay properties of one logical operation.
|
|
@@ -365,11 +335,6 @@ def _entity_optimization_disabled() -> bool:
|
|
|
365
335
|
return cast('bool', module.SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION)
|
|
366
336
|
|
|
367
337
|
|
|
368
|
-
def _escape_disposition(value: str) -> str:
|
|
369
|
-
"""Make a value safe inside a Content-Disposition quoted-string."""
|
|
370
|
-
return value.replace('\\', '\\\\').replace('"', '%22').replace('\r', '').replace('\n', '')
|
|
371
|
-
|
|
372
|
-
|
|
373
338
|
def _explicit_delay_is_safe(delay: float) -> bool:
|
|
374
339
|
"""Return whether a delay stays within the bounded retry policy."""
|
|
375
340
|
return math.isfinite(delay) and 0 <= delay <= _BACKOFF_CAP
|
|
@@ -491,13 +456,6 @@ def _parse_retry_after(headers: Mapping[str, str], now: float) -> _RetryAfter:
|
|
|
491
456
|
return delay
|
|
492
457
|
|
|
493
458
|
|
|
494
|
-
def _validate_multipart_boundary(boundary: str) -> str:
|
|
495
|
-
"""Return a boundary safe for multipart framing and an unquoted header."""
|
|
496
|
-
if not _MULTIPART_BOUNDARY_PATTERN.fullmatch(boundary):
|
|
497
|
-
raise ValueError('multipart boundary must be 1 to 70 safe ASCII characters')
|
|
498
|
-
return boundary
|
|
499
|
-
|
|
500
|
-
|
|
501
459
|
def _validated_nonnegative_integer(value: object, name: str) -> int:
|
|
502
460
|
"""Return a non-boolean, nonnegative integer paging value."""
|
|
503
461
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
@@ -615,57 +573,6 @@ def classify_endpoint(
|
|
|
615
573
|
)
|
|
616
574
|
|
|
617
575
|
|
|
618
|
-
def build_multipart_plan(
|
|
619
|
-
fields: dict[str, str],
|
|
620
|
-
files: list[FileSlot],
|
|
621
|
-
boundary: str | None = None,
|
|
622
|
-
) -> MultipartPlan:
|
|
623
|
-
"""Build a pure multipart/form-data framing plan.
|
|
624
|
-
|
|
625
|
-
Args:
|
|
626
|
-
fields: Text form fields in multipart emission order.
|
|
627
|
-
files: File placeholders in multipart emission order.
|
|
628
|
-
boundary: Explicit boundary of 1-70 ASCII letters, digits, apostrophes,
|
|
629
|
-
plus signs, periods, underscores, or hyphens; `None` generates one.
|
|
630
|
-
|
|
631
|
-
Returns:
|
|
632
|
-
A content type and ordered framing segments for the multipart body.
|
|
633
|
-
|
|
634
|
-
Raises:
|
|
635
|
-
ValueError: If an explicit boundary is empty or unsafe for unquoted
|
|
636
|
-
use.
|
|
637
|
-
"""
|
|
638
|
-
multipart_boundary = (
|
|
639
|
-
_uuid.uuid4().hex if boundary is None else _validate_multipart_boundary(boundary)
|
|
640
|
-
)
|
|
641
|
-
segments: list[bytes | FileSlot] = []
|
|
642
|
-
for name, value in fields.items():
|
|
643
|
-
segments.append(
|
|
644
|
-
(
|
|
645
|
-
f'--{multipart_boundary}\r\nContent-Disposition: form-data; '
|
|
646
|
-
f'name="{_escape_disposition(name)}"'
|
|
647
|
-
f'\r\n\r\n{value}\r\n'
|
|
648
|
-
).encode()
|
|
649
|
-
)
|
|
650
|
-
for slot in files:
|
|
651
|
-
segments.append(
|
|
652
|
-
(
|
|
653
|
-
f'--{multipart_boundary}\r\nContent-Disposition: form-data; '
|
|
654
|
-
f'name="{_escape_disposition(slot.name)}"; '
|
|
655
|
-
f'filename="{_escape_disposition(slot.filename)}"'
|
|
656
|
-
f'\r\nContent-Type: {slot.content_type}'
|
|
657
|
-
'\r\n\r\n'
|
|
658
|
-
).encode()
|
|
659
|
-
)
|
|
660
|
-
segments.append(slot)
|
|
661
|
-
segments.append(b'\r\n')
|
|
662
|
-
segments.append(f'--{multipart_boundary}--\r\n'.encode())
|
|
663
|
-
return MultipartPlan(
|
|
664
|
-
content_type=f'multipart/form-data; boundary={multipart_boundary}',
|
|
665
|
-
segments=segments,
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
|
|
669
576
|
def classify(
|
|
670
577
|
method_name: str,
|
|
671
578
|
normalized_params: object,
|
shotgun_api3/_version.py
CHANGED
|
File without changes
|
|
File without changes
|