sg-python-api 0.1.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.
- sg_python_api-0.1.0.dist-info/METADATA +177 -0
- sg_python_api-0.1.0.dist-info/RECORD +31 -0
- sg_python_api-0.1.0.dist-info/WHEEL +4 -0
- sg_python_api-0.1.0.dist-info/licenses/LICENSE +21 -0
- shotgun_api3/__init__.py +68 -0
- shotgun_api3/_async_client.py +2906 -0
- shotgun_api3/_async_driver.py +945 -0
- shotgun_api3/_async_httpx2.py +662 -0
- shotgun_api3/_async_transport.py +162 -0
- shotgun_api3/_capabilities.py +165 -0
- shotgun_api3/_client.py +2682 -0
- shotgun_api3/_config.py +1463 -0
- shotgun_api3/_crud.py +916 -0
- shotgun_api3/_driver.py +879 -0
- shotgun_api3/_errors.py +49 -0
- shotgun_api3/_execution.py +273 -0
- shotgun_api3/_files.py +1094 -0
- shotgun_api3/_methods.py +1205 -0
- shotgun_api3/_protocol.py +1347 -0
- shotgun_api3/_sentinel.py +5 -0
- shotgun_api3/_sg_timezone.py +5 -0
- shotgun_api3/_signatures.py +53 -0
- shotgun_api3/_transport.py +901 -0
- shotgun_api3/_version.py +3 -0
- shotgun_api3/lib/__init__.py +1 -0
- shotgun_api3/lib/mockgun/__init__.py +16 -0
- shotgun_api3/lib/mockgun/_async.py +315 -0
- shotgun_api3/lib/mockgun/_mockgun.py +1666 -0
- shotgun_api3/lib/sgtimezone.py +209 -0
- shotgun_api3/py.typed +0 -0
- shotgun_api3/shotgun.py +58 -0
shotgun_api3/_client.py
ADDED
|
@@ -0,0 +1,2682 @@
|
|
|
1
|
+
"""Shotgun compatibility facade and client lifecycle."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import dataclasses
|
|
5
|
+
import datetime as _dt_module
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import urllib.request
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Callable, Generator, Iterator, Mapping
|
|
12
|
+
from contextlib import ExitStack, contextmanager, suppress
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import (
|
|
15
|
+
Any,
|
|
16
|
+
Literal,
|
|
17
|
+
Never,
|
|
18
|
+
TypeAlias,
|
|
19
|
+
TypeVar,
|
|
20
|
+
Unpack,
|
|
21
|
+
cast,
|
|
22
|
+
overload,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from . import _config as config_module
|
|
26
|
+
from . import _crud, _errors, _files, _methods, _protocol, _transport
|
|
27
|
+
from ._capabilities import ClientCapabilities, ServerCapabilities
|
|
28
|
+
from ._config import (
|
|
29
|
+
CompatibilityWarningCode,
|
|
30
|
+
Config,
|
|
31
|
+
RequestContext,
|
|
32
|
+
advance_transport_generation,
|
|
33
|
+
constructor_http_proxy_fields,
|
|
34
|
+
readiness_key,
|
|
35
|
+
require_secure_rpc,
|
|
36
|
+
server_host,
|
|
37
|
+
server_page_limit,
|
|
38
|
+
)
|
|
39
|
+
from ._driver import (
|
|
40
|
+
AttemptControl,
|
|
41
|
+
Driver,
|
|
42
|
+
ExecutionOptions,
|
|
43
|
+
LiveResponse,
|
|
44
|
+
)
|
|
45
|
+
from ._errors import ShotgunError
|
|
46
|
+
from ._execution import (
|
|
47
|
+
ExceptionCapture,
|
|
48
|
+
SuppressedExceptionCapture,
|
|
49
|
+
release_lease,
|
|
50
|
+
retire_transport,
|
|
51
|
+
)
|
|
52
|
+
from ._signatures import bind_arguments, expose_signature
|
|
53
|
+
from ._transport import (
|
|
54
|
+
SendError,
|
|
55
|
+
StreamingResponse,
|
|
56
|
+
Transport,
|
|
57
|
+
TransportError,
|
|
58
|
+
TransportRequest,
|
|
59
|
+
TransportResponse,
|
|
60
|
+
Urllib3Transport,
|
|
61
|
+
)
|
|
62
|
+
from ._version import PACKAGE_VERSION
|
|
63
|
+
|
|
64
|
+
_ReadinessKey: TypeAlias = tuple[str, int]
|
|
65
|
+
_UploadResult = TypeVar('_UploadResult')
|
|
66
|
+
|
|
67
|
+
__version__ = PACKAGE_VERSION
|
|
68
|
+
|
|
69
|
+
_CONSTRUCTOR_PARAMETERS = (
|
|
70
|
+
inspect.Parameter('base_url', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
|
|
71
|
+
inspect.Parameter(
|
|
72
|
+
'script_name',
|
|
73
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
74
|
+
default=None,
|
|
75
|
+
annotation=str | None,
|
|
76
|
+
),
|
|
77
|
+
inspect.Parameter(
|
|
78
|
+
'api_key',
|
|
79
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
80
|
+
default=None,
|
|
81
|
+
annotation=str | None,
|
|
82
|
+
),
|
|
83
|
+
inspect.Parameter(
|
|
84
|
+
'convert_datetimes_to_utc',
|
|
85
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
86
|
+
default=True,
|
|
87
|
+
annotation=bool,
|
|
88
|
+
),
|
|
89
|
+
inspect.Parameter(
|
|
90
|
+
'http_proxy',
|
|
91
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
92
|
+
default=None,
|
|
93
|
+
annotation=str | None,
|
|
94
|
+
),
|
|
95
|
+
inspect.Parameter(
|
|
96
|
+
'connect',
|
|
97
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
98
|
+
default=True,
|
|
99
|
+
annotation=bool,
|
|
100
|
+
),
|
|
101
|
+
inspect.Parameter(
|
|
102
|
+
'ca_certs',
|
|
103
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
104
|
+
default=None,
|
|
105
|
+
annotation=str | None,
|
|
106
|
+
),
|
|
107
|
+
inspect.Parameter(
|
|
108
|
+
'login',
|
|
109
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
110
|
+
default=None,
|
|
111
|
+
annotation=str | None,
|
|
112
|
+
),
|
|
113
|
+
inspect.Parameter(
|
|
114
|
+
'password',
|
|
115
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
116
|
+
default=None,
|
|
117
|
+
annotation=str | None,
|
|
118
|
+
),
|
|
119
|
+
inspect.Parameter(
|
|
120
|
+
'sudo_as_login',
|
|
121
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
122
|
+
default=None,
|
|
123
|
+
annotation=str | None,
|
|
124
|
+
),
|
|
125
|
+
inspect.Parameter(
|
|
126
|
+
'session_token',
|
|
127
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
128
|
+
default=None,
|
|
129
|
+
annotation=str | None,
|
|
130
|
+
),
|
|
131
|
+
inspect.Parameter(
|
|
132
|
+
'auth_token',
|
|
133
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
134
|
+
default=None,
|
|
135
|
+
annotation=str | None,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
_UPLOAD_DOCSTRING = """Upload a file or thumbnail and return its Attachment ID.
|
|
139
|
+
|
|
140
|
+
A successful HTTP response from the legacy form-upload route still
|
|
141
|
+
raises `ShotgunError`; no Attachment ID is established for that
|
|
142
|
+
route. The form request has already been sent when this error is
|
|
143
|
+
raised.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
entity_type: Entity type receiving the upload.
|
|
147
|
+
entity_id: Numeric identifier of the receiving entity.
|
|
148
|
+
path: Existing non-empty file path as text or filesystem-encoded bytes.
|
|
149
|
+
field_name: Optional file field receiving an Attachment.
|
|
150
|
+
display_name: Optional Attachment display name.
|
|
151
|
+
tag_list: Optional comma-delimited Attachment tags.
|
|
152
|
+
created_at: Optional Attachment creation timestamp.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Created Attachment ID.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
TypeError: If arguments do not match the documented signature.
|
|
159
|
+
ShotgunError: If the file or an upload response is invalid, an RPC
|
|
160
|
+
fault or terminal transport failure occurs, or the legacy
|
|
161
|
+
form-upload route succeeds.
|
|
162
|
+
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
163
|
+
ValueError: If authenticated HTTP violates the configured origin policy.
|
|
164
|
+
"""
|
|
165
|
+
_UPLOAD_PARAMETERS = (
|
|
166
|
+
inspect.Parameter('entity_type', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
|
|
167
|
+
inspect.Parameter('entity_id', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
|
|
168
|
+
inspect.Parameter('path', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str | bytes),
|
|
169
|
+
inspect.Parameter(
|
|
170
|
+
'field_name',
|
|
171
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
172
|
+
default=None,
|
|
173
|
+
annotation=str | None,
|
|
174
|
+
),
|
|
175
|
+
inspect.Parameter(
|
|
176
|
+
'display_name',
|
|
177
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
178
|
+
default=None,
|
|
179
|
+
annotation=str | None,
|
|
180
|
+
),
|
|
181
|
+
inspect.Parameter(
|
|
182
|
+
'tag_list',
|
|
183
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
184
|
+
default=None,
|
|
185
|
+
annotation=str | None,
|
|
186
|
+
),
|
|
187
|
+
inspect.Parameter(
|
|
188
|
+
'created_at',
|
|
189
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
190
|
+
default=None,
|
|
191
|
+
annotation=_dt_module.datetime | None,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
CONSTRUCTOR_SIGNATURE = inspect.Signature(_CONSTRUCTOR_PARAMETERS, return_annotation=None)
|
|
195
|
+
_METHOD_CONSTRUCTOR_SIGNATURE = inspect.Signature(
|
|
196
|
+
(
|
|
197
|
+
inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
|
198
|
+
*_CONSTRUCTOR_PARAMETERS,
|
|
199
|
+
),
|
|
200
|
+
return_annotation=None,
|
|
201
|
+
)
|
|
202
|
+
_METHOD_UPLOAD_SIGNATURE = inspect.Signature(
|
|
203
|
+
(
|
|
204
|
+
inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD),
|
|
205
|
+
*_UPLOAD_PARAMETERS,
|
|
206
|
+
),
|
|
207
|
+
return_annotation=int,
|
|
208
|
+
)
|
|
209
|
+
_UPLOAD_SIGNATURE = inspect.Signature(_UPLOAD_PARAMETERS, return_annotation=int)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclasses.dataclass(frozen=True)
|
|
213
|
+
class ConstructorOptions:
|
|
214
|
+
"""Collect the exact public constructor arguments for runtime validation.
|
|
215
|
+
|
|
216
|
+
Attributes:
|
|
217
|
+
base_url: Flow Production Tracking site URL.
|
|
218
|
+
script_name: Script name for script authentication.
|
|
219
|
+
api_key: API key for script authentication.
|
|
220
|
+
convert_datetimes_to_utc: Whether to convert datetime values to UTC.
|
|
221
|
+
http_proxy: Optional proxy authority in `[user:pass@]host[:port]` form.
|
|
222
|
+
connect: Whether to request eager readiness during synchronous
|
|
223
|
+
construction or asynchronous context entry.
|
|
224
|
+
ca_certs: Optional certificate-authority bundle path.
|
|
225
|
+
login: Login for human-user authentication.
|
|
226
|
+
password: Password for human-user authentication.
|
|
227
|
+
sudo_as_login: Optional user login for sudo impersonation.
|
|
228
|
+
session_token: Existing session token for authentication.
|
|
229
|
+
auth_token: Optional two-factor authentication token.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
base_url: str
|
|
233
|
+
script_name: str | None = None
|
|
234
|
+
api_key: str | None = None
|
|
235
|
+
convert_datetimes_to_utc: bool = True
|
|
236
|
+
http_proxy: str | None = None
|
|
237
|
+
connect: bool = True
|
|
238
|
+
ca_certs: str | None = None
|
|
239
|
+
login: str | None = None
|
|
240
|
+
password: str | None = None
|
|
241
|
+
sudo_as_login: str | None = None
|
|
242
|
+
session_token: str | None = None
|
|
243
|
+
auth_token: str | None = None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class _DownloadCleanupError(Exception):
|
|
247
|
+
"""Keep required download cleanup failures outside transport retry policy."""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclasses.dataclass(frozen=True)
|
|
251
|
+
class _DownloadExchange:
|
|
252
|
+
"""Own one restartable streaming download workflow."""
|
|
253
|
+
|
|
254
|
+
context: RequestContext
|
|
255
|
+
file_path: str | None
|
|
256
|
+
headers: Mapping[str, str] = dataclasses.field(repr=False)
|
|
257
|
+
url: str = dataclasses.field(repr=False)
|
|
258
|
+
|
|
259
|
+
def _redirect(self, response: LiveResponse) -> TransportRequest:
|
|
260
|
+
"""Build one credential-free explicit redirect request."""
|
|
261
|
+
return _files.build_download_redirect(self.context, response.headers)
|
|
262
|
+
|
|
263
|
+
def _write_file(
|
|
264
|
+
self,
|
|
265
|
+
chunks: Iterator[bytes],
|
|
266
|
+
control: AttemptControl,
|
|
267
|
+
total_bytes: int | None,
|
|
268
|
+
) -> str:
|
|
269
|
+
"""Publish one fully consumed attempt through an adjacent temp file."""
|
|
270
|
+
if self.file_path is None:
|
|
271
|
+
raise AssertionError('file download requires a destination')
|
|
272
|
+
destination = Path(self.file_path)
|
|
273
|
+
stack: ExitStack | None = ExitStack()
|
|
274
|
+
temporary: Path | None = None
|
|
275
|
+
transfer = ExceptionCapture()
|
|
276
|
+
try:
|
|
277
|
+
with transfer:
|
|
278
|
+
handle = stack.enter_context(_files.download_temporary(destination))
|
|
279
|
+
temporary = Path(handle.name)
|
|
280
|
+
control.consume_into(
|
|
281
|
+
chunks,
|
|
282
|
+
handle.write,
|
|
283
|
+
total_bytes=total_bytes,
|
|
284
|
+
)
|
|
285
|
+
opened_stack = stack
|
|
286
|
+
stack = None
|
|
287
|
+
opened_cleanup = SuppressedExceptionCapture()
|
|
288
|
+
with opened_cleanup:
|
|
289
|
+
opened_stack.close()
|
|
290
|
+
if isinstance(opened_cleanup.exception, Exception):
|
|
291
|
+
raise _DownloadCleanupError from None
|
|
292
|
+
if opened_cleanup.exception is not None:
|
|
293
|
+
raise opened_cleanup.exception
|
|
294
|
+
temporary.replace(destination)
|
|
295
|
+
finally:
|
|
296
|
+
close_cleanup = SuppressedExceptionCapture()
|
|
297
|
+
unlink_cleanup = SuppressedExceptionCapture()
|
|
298
|
+
if stack is not None:
|
|
299
|
+
with close_cleanup:
|
|
300
|
+
stack.close()
|
|
301
|
+
if temporary is not None:
|
|
302
|
+
with unlink_cleanup:
|
|
303
|
+
temporary.unlink(missing_ok=True)
|
|
304
|
+
if transfer.exception is None:
|
|
305
|
+
for cleanup_error in (close_cleanup.exception, unlink_cleanup.exception):
|
|
306
|
+
if isinstance(cleanup_error, Exception):
|
|
307
|
+
raise _DownloadCleanupError from None
|
|
308
|
+
if cleanup_error is not None:
|
|
309
|
+
raise cleanup_error
|
|
310
|
+
return self.file_path
|
|
311
|
+
|
|
312
|
+
def _consume(self, response: LiveResponse, control: AttemptControl) -> bytes | str:
|
|
313
|
+
"""Consume a successful streaming response into attempt-local output."""
|
|
314
|
+
if not _protocol.is_http_success(response.status):
|
|
315
|
+
raise _errors.ShotgunFileDownloadError('attachment download failed')
|
|
316
|
+
streaming = cast('StreamingResponse', response)
|
|
317
|
+
chunks = streaming.iter_chunks()
|
|
318
|
+
total_bytes = _transport.content_length(response.headers)
|
|
319
|
+
if self.file_path is None:
|
|
320
|
+
return control.consume(chunks, total_bytes=total_bytes)
|
|
321
|
+
return self._write_file(chunks, control, total_bytes)
|
|
322
|
+
|
|
323
|
+
def _execute(
|
|
324
|
+
self,
|
|
325
|
+
driver: Driver,
|
|
326
|
+
operation: _protocol.OperationDescriptor,
|
|
327
|
+
) -> bytes | str:
|
|
328
|
+
"""Execute the workflow through the common streaming attempt loop."""
|
|
329
|
+
try:
|
|
330
|
+
return driver.execute(
|
|
331
|
+
operation,
|
|
332
|
+
self.context,
|
|
333
|
+
lambda _attempt: _files.build_download_request(
|
|
334
|
+
self.context, self.url, self.headers
|
|
335
|
+
),
|
|
336
|
+
self._consume,
|
|
337
|
+
options=ExecutionOptions(
|
|
338
|
+
redirect_factory=self._redirect,
|
|
339
|
+
streaming=True,
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
except _DownloadCleanupError:
|
|
343
|
+
raise SendError('download cleanup failed') from None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dataclasses.dataclass(frozen=True)
|
|
347
|
+
class _InvocationOptions:
|
|
348
|
+
"""Collect non-payload controls for one client invocation."""
|
|
349
|
+
|
|
350
|
+
include_auth: bool = True
|
|
351
|
+
compatibility_warning_codes: tuple[CompatibilityWarningCode, ...] = ()
|
|
352
|
+
logical_method_name: str | None = None
|
|
353
|
+
auth_spec: _protocol.AuthSpec | None = None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _bind_upload(
|
|
357
|
+
args: tuple[object, ...],
|
|
358
|
+
kwargs: dict[str, object],
|
|
359
|
+
) -> _files.UploadArguments:
|
|
360
|
+
"""Bind runtime arguments with upload-specific errors."""
|
|
361
|
+
bound = bind_arguments(_UPLOAD_SIGNATURE, args, kwargs, 'Shotgun.upload()')
|
|
362
|
+
arguments = cast('Callable[..., _files.UploadArguments]', _files.UploadArguments)
|
|
363
|
+
return arguments(**bound.arguments)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _execute_upload_exchange(
|
|
367
|
+
driver: Driver,
|
|
368
|
+
context: RequestContext,
|
|
369
|
+
exchange: _files.UploadExchange,
|
|
370
|
+
parser: Callable[[str, Mapping[str, str]], _UploadResult],
|
|
371
|
+
) -> _UploadResult:
|
|
372
|
+
"""Run one upload exchange through the common attempt executor."""
|
|
373
|
+
|
|
374
|
+
def request_factory(_attempt: int) -> TransportRequest:
|
|
375
|
+
return TransportRequest(
|
|
376
|
+
method=exchange.method,
|
|
377
|
+
url=exchange.url,
|
|
378
|
+
headers=dict(exchange.headers),
|
|
379
|
+
body=exchange.body_factory(),
|
|
380
|
+
timeout=context.timeout,
|
|
381
|
+
token=uuid.uuid4().hex,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def consume(
|
|
385
|
+
response: LiveResponse,
|
|
386
|
+
control: AttemptControl,
|
|
387
|
+
) -> _UploadResult:
|
|
388
|
+
buffered = cast('TransportResponse', response)
|
|
389
|
+
_protocol.check_http_status(
|
|
390
|
+
buffered.status,
|
|
391
|
+
buffered.reason,
|
|
392
|
+
context.canonical_origin or '',
|
|
393
|
+
buffered.headers,
|
|
394
|
+
buffered.content,
|
|
395
|
+
)
|
|
396
|
+
body = control.consume((buffered.content,), total_bytes=len(buffered.content))
|
|
397
|
+
return parser(body.decode('utf-8', errors='replace'), buffered.headers)
|
|
398
|
+
|
|
399
|
+
return driver.execute(
|
|
400
|
+
exchange.operation,
|
|
401
|
+
context,
|
|
402
|
+
request_factory,
|
|
403
|
+
consume,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _expose_upload_signature(function: Callable[..., object]) -> Callable[..., object]:
|
|
408
|
+
"""Expose the observed upload signature on its variadic implementation."""
|
|
409
|
+
return expose_signature(function, _METHOD_UPLOAD_SIGNATURE, _UPLOAD_DOCSTRING)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def bind_constructor(
|
|
413
|
+
args: tuple[object, ...],
|
|
414
|
+
kwargs: dict[str, object],
|
|
415
|
+
client_name: str,
|
|
416
|
+
) -> ConstructorOptions:
|
|
417
|
+
"""Bind constructor arguments with client-specific errors.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
args: Positional public arguments.
|
|
421
|
+
kwargs: Keyword public arguments.
|
|
422
|
+
client_name: Public client class name for error attribution.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Bound immutable constructor options.
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
TypeError: If the arguments do not match the public signature.
|
|
429
|
+
"""
|
|
430
|
+
bound = bind_arguments(CONSTRUCTOR_SIGNATURE, args, kwargs, f'{client_name}.__init__()')
|
|
431
|
+
constructor = cast('Callable[..., ConstructorOptions]', ConstructorOptions)
|
|
432
|
+
return constructor(**bound.arguments)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def build_config(options: ConstructorOptions) -> Config:
|
|
436
|
+
"""Initialize and validate network-free configuration, auth, and proxy fields.
|
|
437
|
+
|
|
438
|
+
Raises:
|
|
439
|
+
TypeError: If a configuration or proxy value has an unsupported type.
|
|
440
|
+
ValueError: If authentication, site or proxy values, or
|
|
441
|
+
`SHOTGUN_API_RETRY_INTERVAL` are invalid.
|
|
442
|
+
"""
|
|
443
|
+
config = Config()
|
|
444
|
+
config.set_server_params(options.base_url)
|
|
445
|
+
config.update_fields(
|
|
446
|
+
api_key=options.api_key,
|
|
447
|
+
auth_token=options.auth_token,
|
|
448
|
+
convert_datetimes_to_utc=options.convert_datetimes_to_utc,
|
|
449
|
+
script_name=options.script_name,
|
|
450
|
+
session_token=options.session_token,
|
|
451
|
+
sudo_as_login=options.sudo_as_login,
|
|
452
|
+
user_login=options.login,
|
|
453
|
+
user_password=options.password,
|
|
454
|
+
)
|
|
455
|
+
_protocol.validate_auth(config_module.auth_spec(config))
|
|
456
|
+
proxy_fields = constructor_http_proxy_fields(options.http_proxy)
|
|
457
|
+
if proxy_fields is not None:
|
|
458
|
+
config.update_fields(**proxy_fields)
|
|
459
|
+
return config
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def expose_constructor_signature(function: Callable[..., object]) -> Callable[..., object]:
|
|
463
|
+
"""Expose the documented constructor signature.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
function: Variadic constructor implementation.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The same function with its public runtime signature attached.
|
|
470
|
+
"""
|
|
471
|
+
expose_signature(function, _METHOD_CONSTRUCTOR_SIGNATURE, None)
|
|
472
|
+
annotations = {
|
|
473
|
+
name: parameter.annotation
|
|
474
|
+
for name, parameter in CONSTRUCTOR_SIGNATURE.parameters.items()
|
|
475
|
+
if parameter.annotation is not inspect.Parameter.empty
|
|
476
|
+
}
|
|
477
|
+
annotations['return'] = CONSTRUCTOR_SIGNATURE.return_annotation
|
|
478
|
+
object.__setattr__(function, '__annotations__', annotations)
|
|
479
|
+
return function
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class Shotgun:
|
|
483
|
+
"""Provide the synchronous Flow Production Tracking API client surface.
|
|
484
|
+
|
|
485
|
+
Instances may be shared across threads. Call `close()` to retire the
|
|
486
|
+
current transport; in-flight work retains its leases until completion.
|
|
487
|
+
A later operation reconnects lazily.
|
|
488
|
+
|
|
489
|
+
Attributes:
|
|
490
|
+
base_url: The constructor's normalized base URL.
|
|
491
|
+
client_caps: Capabilities of this Python client.
|
|
492
|
+
config: The writable client configuration.
|
|
493
|
+
server_caps: Validated capabilities for the configured origin.
|
|
494
|
+
server_info: Raw validated server metadata.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
__signature__ = CONSTRUCTOR_SIGNATURE
|
|
498
|
+
|
|
499
|
+
@overload
|
|
500
|
+
def __init__(self, base_url: Never) -> None: ...
|
|
501
|
+
|
|
502
|
+
@overload
|
|
503
|
+
def __init__(
|
|
504
|
+
self,
|
|
505
|
+
base_url: str,
|
|
506
|
+
script_name: str | None = None,
|
|
507
|
+
api_key: str | None = None,
|
|
508
|
+
convert_datetimes_to_utc: bool = True,
|
|
509
|
+
http_proxy: str | None = None,
|
|
510
|
+
connect: bool = True,
|
|
511
|
+
ca_certs: str | None = None,
|
|
512
|
+
login: str | None = None,
|
|
513
|
+
password: str | None = None,
|
|
514
|
+
sudo_as_login: str | None = None,
|
|
515
|
+
session_token: str | None = None,
|
|
516
|
+
auth_token: str | None = None,
|
|
517
|
+
) -> None: ...
|
|
518
|
+
|
|
519
|
+
@expose_constructor_signature
|
|
520
|
+
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
521
|
+
"""Initialize a client for one site and authentication mode.
|
|
522
|
+
|
|
523
|
+
Supply exactly one authentication mode: `script_name` with `api_key`,
|
|
524
|
+
`login` with `password`, or `session_token`. An optional one-use
|
|
525
|
+
`auth_token` requires `login` and `password`.
|
|
526
|
+
|
|
527
|
+
`base_url` must identify an HTTP or HTTPS site without embedded
|
|
528
|
+
credentials. Authenticated remote operations require HTTPS.
|
|
529
|
+
`http_proxy` accepts `[user:pass@]host[:port]`, with port 8080 when
|
|
530
|
+
omitted. A truthy `ca_certs` selects a CA bundle; otherwise
|
|
531
|
+
`SHOTGUN_API_CACERTS` is consulted before using system trust roots.
|
|
532
|
+
|
|
533
|
+
Construction performs server-readiness I/O by default. Supplying
|
|
534
|
+
`auth_token` also performs a session-token exchange during
|
|
535
|
+
construction, even when `connect` is `False`.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
*args: Positional values from the documented constructor signature.
|
|
539
|
+
**kwargs: Keyword values from the documented constructor signature.
|
|
540
|
+
|
|
541
|
+
Raises:
|
|
542
|
+
TypeError: If arguments do not match the documented signature.
|
|
543
|
+
ValueError: If authentication, URL, or proxy values are invalid.
|
|
544
|
+
ShotgunError: If authentication, server metadata, or an RPC result
|
|
545
|
+
is invalid, or a transport attempt fails.
|
|
546
|
+
ProtocolError: If eager readiness or token exchange receives an
|
|
547
|
+
HTTP error.
|
|
548
|
+
"""
|
|
549
|
+
self._initialize(bind_constructor(args, kwargs, 'Shotgun'))
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def _connection(self) -> Transport | None:
|
|
553
|
+
"""Return the current private connection compatibility view."""
|
|
554
|
+
with self._transport_lock:
|
|
555
|
+
return self._transport
|
|
556
|
+
|
|
557
|
+
def _invoke(
|
|
558
|
+
self,
|
|
559
|
+
driver: Driver,
|
|
560
|
+
method_name: str,
|
|
561
|
+
params: object,
|
|
562
|
+
context: RequestContext,
|
|
563
|
+
options: _InvocationOptions,
|
|
564
|
+
) -> object:
|
|
565
|
+
"""Run one transformed RPC using only its immutable snapshot."""
|
|
566
|
+
require_secure_rpc(context, options.include_auth)
|
|
567
|
+
outbound = _protocol.transform_outbound(params, context.convert_datetimes_to_utc)
|
|
568
|
+
result = driver.call(
|
|
569
|
+
method_name,
|
|
570
|
+
outbound,
|
|
571
|
+
context,
|
|
572
|
+
include_auth=options.include_auth,
|
|
573
|
+
compatibility_warning_codes=options.compatibility_warning_codes,
|
|
574
|
+
_logical_method_name=options.logical_method_name,
|
|
575
|
+
)
|
|
576
|
+
return _protocol.transform_inbound(result, context.convert_datetimes_to_utc)
|
|
577
|
+
|
|
578
|
+
def _new_transport(self, context: RequestContext) -> Transport:
|
|
579
|
+
"""Create a transport for one captured generation."""
|
|
580
|
+
return Urllib3Transport(
|
|
581
|
+
ca_certs=self._ca_certs,
|
|
582
|
+
proxy_headers=dict(context.proxy_headers) or None,
|
|
583
|
+
proxy_url=context.proxy_url,
|
|
584
|
+
pool_maxsize=context.pool_maxsize,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
def _publish_capabilities(
|
|
588
|
+
self,
|
|
589
|
+
context: RequestContext,
|
|
590
|
+
capabilities: ServerCapabilities,
|
|
591
|
+
) -> bool:
|
|
592
|
+
"""Publish metadata only while its captured origin remains current."""
|
|
593
|
+
return config_module.publish_server_info(self._config, context, capabilities.server_info)
|
|
594
|
+
|
|
595
|
+
def _release_transport(self, transport: Transport) -> None:
|
|
596
|
+
"""Release a lease and close a retired transport at its safe point."""
|
|
597
|
+
with self._transport_lock:
|
|
598
|
+
close_after_unlock = release_lease(
|
|
599
|
+
self._lease_counts, self._retired_transports, transport
|
|
600
|
+
)
|
|
601
|
+
if close_after_unlock is not None:
|
|
602
|
+
close_after_unlock.close()
|
|
603
|
+
|
|
604
|
+
def _release_transport_if_retired_close_fails(
|
|
605
|
+
self,
|
|
606
|
+
transport: Transport,
|
|
607
|
+
retired_transports: list[Transport],
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Close retired transports or roll back the replacement lease."""
|
|
610
|
+
try:
|
|
611
|
+
for retired in retired_transports:
|
|
612
|
+
retired.close()
|
|
613
|
+
except BaseException:
|
|
614
|
+
with suppress(BaseException):
|
|
615
|
+
self._release_transport(transport)
|
|
616
|
+
raise
|
|
617
|
+
|
|
618
|
+
def _acquire_driver(self, context: RequestContext) -> tuple[Driver, Transport]:
|
|
619
|
+
"""Lease the driver and transport for one complete logical operation."""
|
|
620
|
+
close_after_unlock: list[Transport] = []
|
|
621
|
+
with self._transport_lock:
|
|
622
|
+
if self._known_transport is not self._transport:
|
|
623
|
+
if self._known_transport is not None:
|
|
624
|
+
retired = retire_transport(
|
|
625
|
+
self._lease_counts, self._retired_transports, self._known_transport
|
|
626
|
+
)
|
|
627
|
+
if retired is not None:
|
|
628
|
+
close_after_unlock.append(retired)
|
|
629
|
+
self._known_transport = self._transport
|
|
630
|
+
self._driver = None
|
|
631
|
+
self._driver_generation = None
|
|
632
|
+
self._transport_owned = False
|
|
633
|
+
|
|
634
|
+
transport = self._transport
|
|
635
|
+
if transport is None:
|
|
636
|
+
transport = self._new_transport(context)
|
|
637
|
+
self._transport = transport
|
|
638
|
+
self._known_transport = transport
|
|
639
|
+
self._transport_owned = True
|
|
640
|
+
elif self._driver_generation not in {None, context.transport_generation}:
|
|
641
|
+
if self._transport_owned:
|
|
642
|
+
retired = retire_transport(
|
|
643
|
+
self._lease_counts, self._retired_transports, transport
|
|
644
|
+
)
|
|
645
|
+
if retired is not None:
|
|
646
|
+
close_after_unlock.append(retired)
|
|
647
|
+
transport = self._new_transport(context)
|
|
648
|
+
self._transport = transport
|
|
649
|
+
self._known_transport = transport
|
|
650
|
+
self._driver = None
|
|
651
|
+
|
|
652
|
+
if self._driver is None:
|
|
653
|
+
self._driver = Driver(transport)
|
|
654
|
+
self._driver_generation = context.transport_generation
|
|
655
|
+
key = id(transport)
|
|
656
|
+
self._lease_counts[key] = self._lease_counts.get(key, 0) + 1
|
|
657
|
+
driver = self._driver
|
|
658
|
+
self._release_transport_if_retired_close_fails(transport, close_after_unlock)
|
|
659
|
+
return driver, transport
|
|
660
|
+
|
|
661
|
+
@contextmanager
|
|
662
|
+
def _driver_lease(self, context: RequestContext) -> Generator[Driver, None, None]:
|
|
663
|
+
"""Keep one transport alive across readiness and operation I/O."""
|
|
664
|
+
driver, transport = self._acquire_driver(context)
|
|
665
|
+
try:
|
|
666
|
+
yield driver
|
|
667
|
+
finally:
|
|
668
|
+
self._release_transport(transport)
|
|
669
|
+
|
|
670
|
+
def _run_info(
|
|
671
|
+
self,
|
|
672
|
+
context: RequestContext,
|
|
673
|
+
driver: Driver,
|
|
674
|
+
*,
|
|
675
|
+
accept_ready: bool,
|
|
676
|
+
) -> tuple[dict[str, object], ServerCapabilities]:
|
|
677
|
+
"""Own, validate, and publish one readiness metadata exchange."""
|
|
678
|
+
key = readiness_key(context)
|
|
679
|
+
owner = threading.get_ident()
|
|
680
|
+
with self._readiness:
|
|
681
|
+
while key in self._readiness_in_progress:
|
|
682
|
+
if self._readiness_owner[key] == owner:
|
|
683
|
+
raise RuntimeError('readiness cannot recursively wait on itself')
|
|
684
|
+
self._readiness.wait()
|
|
685
|
+
cached = self._ready_caps.get(key)
|
|
686
|
+
if accept_ready and cached is not None:
|
|
687
|
+
return cached.server_info, cached
|
|
688
|
+
self._readiness_in_progress.add(key)
|
|
689
|
+
self._readiness_owner[key] = owner
|
|
690
|
+
|
|
691
|
+
capabilities: ServerCapabilities | None = None
|
|
692
|
+
published = False
|
|
693
|
+
try:
|
|
694
|
+
result = self._invoke(
|
|
695
|
+
driver,
|
|
696
|
+
'info',
|
|
697
|
+
None,
|
|
698
|
+
context,
|
|
699
|
+
_InvocationOptions(include_auth=False),
|
|
700
|
+
)
|
|
701
|
+
if not isinstance(result, dict):
|
|
702
|
+
raise ShotgunError(
|
|
703
|
+
'The Flow Production Tracking server returned invalid metadata.'
|
|
704
|
+
)
|
|
705
|
+
metadata = cast('dict[str, object]', result)
|
|
706
|
+
server_page_limit(metadata)
|
|
707
|
+
capabilities = ServerCapabilities(server_host(context), metadata)
|
|
708
|
+
published = self._publish_capabilities(context, capabilities)
|
|
709
|
+
return metadata, capabilities
|
|
710
|
+
finally:
|
|
711
|
+
with self._readiness:
|
|
712
|
+
if self._readiness_owner.get(key) == owner:
|
|
713
|
+
self._readiness_in_progress.discard(key)
|
|
714
|
+
self._readiness_owner.pop(key)
|
|
715
|
+
current_key = config_module.readiness_identity(self._config)
|
|
716
|
+
if capabilities is not None and published and key == current_key:
|
|
717
|
+
self._ready_caps.clear()
|
|
718
|
+
self._ready_caps[key] = capabilities
|
|
719
|
+
self._readiness.notify_all()
|
|
720
|
+
|
|
721
|
+
def _ensure_readiness(
|
|
722
|
+
self,
|
|
723
|
+
context: RequestContext,
|
|
724
|
+
driver: Driver,
|
|
725
|
+
) -> ServerCapabilities:
|
|
726
|
+
"""Validate one origin while coalescing concurrent callers."""
|
|
727
|
+
_meta, capabilities = self._run_info(context, driver, accept_ready=True)
|
|
728
|
+
return capabilities
|
|
729
|
+
|
|
730
|
+
def _call(
|
|
731
|
+
self,
|
|
732
|
+
method_name: str,
|
|
733
|
+
params: object,
|
|
734
|
+
invocation_options: _InvocationOptions | None = None,
|
|
735
|
+
) -> object:
|
|
736
|
+
"""Run one RPC from a coherent request snapshot."""
|
|
737
|
+
owned_params = copy.deepcopy(params)
|
|
738
|
+
options = invocation_options or _InvocationOptions()
|
|
739
|
+
context = config_module.snapshot(self._config, options.auth_spec)
|
|
740
|
+
if method_name != 'info':
|
|
741
|
+
require_secure_rpc(context, options.include_auth)
|
|
742
|
+
with self._driver_lease(context) as driver:
|
|
743
|
+
if method_name == 'info':
|
|
744
|
+
result, _capabilities = self._run_info(
|
|
745
|
+
context,
|
|
746
|
+
driver,
|
|
747
|
+
accept_ready=False,
|
|
748
|
+
)
|
|
749
|
+
return result
|
|
750
|
+
self._ensure_readiness(context, driver)
|
|
751
|
+
return self._invoke(
|
|
752
|
+
driver,
|
|
753
|
+
method_name,
|
|
754
|
+
owned_params,
|
|
755
|
+
context,
|
|
756
|
+
options,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
def _get_session_token_for(self, context: RequestContext, driver: Driver) -> str:
|
|
760
|
+
"""Return a token obtained entirely from one captured request state."""
|
|
761
|
+
if context.session_token is not None:
|
|
762
|
+
return context.session_token
|
|
763
|
+
self._ensure_readiness(context, driver)
|
|
764
|
+
result = self._invoke(
|
|
765
|
+
driver,
|
|
766
|
+
'get_session_token',
|
|
767
|
+
None,
|
|
768
|
+
context,
|
|
769
|
+
_InvocationOptions(),
|
|
770
|
+
)
|
|
771
|
+
if not isinstance(result, dict):
|
|
772
|
+
raise ShotgunError('get_session_token did not return a session_id')
|
|
773
|
+
token_result = cast('dict[str, object]', result)
|
|
774
|
+
session_id = token_result.get('session_id')
|
|
775
|
+
if not isinstance(session_id, str):
|
|
776
|
+
raise ShotgunError('get_session_token did not return a session_id')
|
|
777
|
+
config_module.publish_session_token(self._config, context, session_id)
|
|
778
|
+
return session_id
|
|
779
|
+
|
|
780
|
+
@staticmethod
|
|
781
|
+
def _share_thumbnail_source(
|
|
782
|
+
driver: Driver,
|
|
783
|
+
context: RequestContext,
|
|
784
|
+
entities: list[Mapping[str, object]],
|
|
785
|
+
source_entity: Mapping[str, object],
|
|
786
|
+
filmstrip_thumbnail: bool,
|
|
787
|
+
) -> int:
|
|
788
|
+
"""Share one captured source thumbnail through the upload endpoint."""
|
|
789
|
+
fields = _files.add_auth_fields(
|
|
790
|
+
_files.build_share_thumbnail_fields(
|
|
791
|
+
entities,
|
|
792
|
+
source_entity,
|
|
793
|
+
filmstrip_thumbnail,
|
|
794
|
+
),
|
|
795
|
+
context.auth,
|
|
796
|
+
)
|
|
797
|
+
return _execute_upload_exchange(
|
|
798
|
+
driver,
|
|
799
|
+
context,
|
|
800
|
+
_files.build_upload_endpoint_exchange(
|
|
801
|
+
context.canonical_origin,
|
|
802
|
+
context.user_agent,
|
|
803
|
+
_files.SHARE_THUMBNAIL_OPERATION,
|
|
804
|
+
_files.SHARE_THUMBNAIL,
|
|
805
|
+
fields,
|
|
806
|
+
),
|
|
807
|
+
lambda text, _headers: _files.parse_link_completion(text),
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
def _thumbnail_source_ready(
|
|
811
|
+
self,
|
|
812
|
+
driver: Driver,
|
|
813
|
+
context: RequestContext,
|
|
814
|
+
source_entity_type: str,
|
|
815
|
+
source_entity_id: int,
|
|
816
|
+
field_name: Literal['filmstrip_image', 'image'],
|
|
817
|
+
) -> bool:
|
|
818
|
+
"""Return whether one captured thumbnail field is no longer transient."""
|
|
819
|
+
params = _crud.build_thumbnail_read_params(
|
|
820
|
+
source_entity_type, source_entity_id, field_name, context.records_per_page
|
|
821
|
+
)
|
|
822
|
+
result = self._invoke(
|
|
823
|
+
driver,
|
|
824
|
+
'read',
|
|
825
|
+
params,
|
|
826
|
+
context,
|
|
827
|
+
_InvocationOptions(),
|
|
828
|
+
)
|
|
829
|
+
return _files.thumbnail_source_ready(result, field_name)
|
|
830
|
+
|
|
831
|
+
@staticmethod
|
|
832
|
+
def _upload_allocation(
|
|
833
|
+
driver: Driver,
|
|
834
|
+
context: RequestContext,
|
|
835
|
+
prepared: _files.PreparedUpload,
|
|
836
|
+
multipart: bool,
|
|
837
|
+
) -> _files.UploadLinkInfo:
|
|
838
|
+
"""Allocate one direct or multipart upload."""
|
|
839
|
+
options = prepared.options
|
|
840
|
+
arguments = options.arguments
|
|
841
|
+
fields = _files.add_auth_fields(
|
|
842
|
+
_files.build_upload_link_fields(
|
|
843
|
+
prepared.filename,
|
|
844
|
+
options.upload_type,
|
|
845
|
+
multipart,
|
|
846
|
+
arguments.created_at,
|
|
847
|
+
),
|
|
848
|
+
context.auth,
|
|
849
|
+
)
|
|
850
|
+
return _execute_upload_exchange(
|
|
851
|
+
driver,
|
|
852
|
+
context,
|
|
853
|
+
_files.build_upload_endpoint_exchange(
|
|
854
|
+
context.canonical_origin,
|
|
855
|
+
context.user_agent,
|
|
856
|
+
_protocol.classify_endpoint('allocation'),
|
|
857
|
+
_files.GET_UPLOAD_LINK_INFO,
|
|
858
|
+
fields,
|
|
859
|
+
),
|
|
860
|
+
lambda text, _headers: _files.UploadLinkInfo(text, multipart=multipart),
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
@staticmethod
|
|
864
|
+
def _upload_direct(
|
|
865
|
+
driver: Driver,
|
|
866
|
+
context: RequestContext,
|
|
867
|
+
prepared: _files.PreparedUpload,
|
|
868
|
+
upload_info: _files.UploadLinkInfo,
|
|
869
|
+
) -> None:
|
|
870
|
+
"""Stream one complete file to stable object storage."""
|
|
871
|
+
_execute_upload_exchange(
|
|
872
|
+
driver,
|
|
873
|
+
context,
|
|
874
|
+
_files.build_storage_upload_exchange(
|
|
875
|
+
context.user_agent,
|
|
876
|
+
_protocol.classify_endpoint('object_put'),
|
|
877
|
+
upload_info.upload_url,
|
|
878
|
+
_files.StorageRange(
|
|
879
|
+
length=prepared.file_size,
|
|
880
|
+
offset=0,
|
|
881
|
+
path=prepared.options.arguments.path,
|
|
882
|
+
),
|
|
883
|
+
),
|
|
884
|
+
lambda _text, _headers: None,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
@staticmethod
|
|
888
|
+
def _upload_form(
|
|
889
|
+
driver: Driver,
|
|
890
|
+
context: RequestContext,
|
|
891
|
+
prepared: _files.PreparedUpload,
|
|
892
|
+
) -> Never:
|
|
893
|
+
"""Send the observed form request without guessing a success result."""
|
|
894
|
+
if context.canonical_origin is None:
|
|
895
|
+
raise ValueError('server parameters must be configured before upload I/O')
|
|
896
|
+
exchange = _files.build_form_upload_exchange(
|
|
897
|
+
context.canonical_origin,
|
|
898
|
+
context.user_agent,
|
|
899
|
+
prepared,
|
|
900
|
+
context.auth,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
def reject_success(_text: str, _headers: Mapping[str, str]) -> Never:
|
|
904
|
+
raise ShotgunError('form upload success response is not established')
|
|
905
|
+
|
|
906
|
+
_execute_upload_exchange(driver, context, exchange, reject_success)
|
|
907
|
+
raise AssertionError('form upload response parser returned')
|
|
908
|
+
|
|
909
|
+
@staticmethod
|
|
910
|
+
def _upload_link(
|
|
911
|
+
driver: Driver,
|
|
912
|
+
context: RequestContext,
|
|
913
|
+
prepared: _files.PreparedUpload,
|
|
914
|
+
upload_info: _files.UploadLinkInfo,
|
|
915
|
+
) -> int:
|
|
916
|
+
"""Link a stored object to its receiving entity."""
|
|
917
|
+
fields = _files.add_auth_fields(
|
|
918
|
+
_files.build_link_file_fields(
|
|
919
|
+
_files.build_link_entity_fields(prepared),
|
|
920
|
+
upload_info.encoded,
|
|
921
|
+
),
|
|
922
|
+
context.auth,
|
|
923
|
+
)
|
|
924
|
+
return _execute_upload_exchange(
|
|
925
|
+
driver,
|
|
926
|
+
context,
|
|
927
|
+
_files.build_upload_endpoint_exchange(
|
|
928
|
+
context.canonical_origin,
|
|
929
|
+
context.user_agent,
|
|
930
|
+
_protocol.classify_endpoint('link'),
|
|
931
|
+
_files.LINK_FILE,
|
|
932
|
+
fields,
|
|
933
|
+
),
|
|
934
|
+
lambda text, _headers: _files.parse_link_completion(text),
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
@staticmethod
|
|
938
|
+
def _upload_multipart(
|
|
939
|
+
driver: Driver,
|
|
940
|
+
context: RequestContext,
|
|
941
|
+
prepared: _files.PreparedUpload,
|
|
942
|
+
upload_info: _files.UploadLinkInfo,
|
|
943
|
+
) -> None:
|
|
944
|
+
"""Stream ordered parts and finalize one multipart allocation."""
|
|
945
|
+
etags: list[str] = []
|
|
946
|
+
for part_number, storage_range in enumerate(
|
|
947
|
+
_files.multipart_ranges(prepared),
|
|
948
|
+
start=1,
|
|
949
|
+
):
|
|
950
|
+
fields = _files.add_auth_fields(
|
|
951
|
+
_files.build_part_fields(upload_info, prepared.filename, part_number),
|
|
952
|
+
context.auth,
|
|
953
|
+
)
|
|
954
|
+
part_url = _execute_upload_exchange(
|
|
955
|
+
driver,
|
|
956
|
+
context,
|
|
957
|
+
_files.build_upload_endpoint_exchange(
|
|
958
|
+
context.canonical_origin,
|
|
959
|
+
context.user_agent,
|
|
960
|
+
_protocol.classify_endpoint('allocation'),
|
|
961
|
+
_files.GET_PART_LINK,
|
|
962
|
+
fields,
|
|
963
|
+
),
|
|
964
|
+
lambda text, _headers: _files.parse_part_link(text),
|
|
965
|
+
)
|
|
966
|
+
etag = _execute_upload_exchange(
|
|
967
|
+
driver,
|
|
968
|
+
context,
|
|
969
|
+
_files.build_storage_upload_exchange(
|
|
970
|
+
context.user_agent,
|
|
971
|
+
_protocol.classify_endpoint('part_put'),
|
|
972
|
+
part_url,
|
|
973
|
+
storage_range,
|
|
974
|
+
),
|
|
975
|
+
_files.parse_part_etag,
|
|
976
|
+
)
|
|
977
|
+
etags.append(etag)
|
|
978
|
+
|
|
979
|
+
fields = _files.add_auth_fields(
|
|
980
|
+
_files.build_complete_fields(upload_info, prepared.filename, etags),
|
|
981
|
+
context.auth,
|
|
982
|
+
)
|
|
983
|
+
_execute_upload_exchange(
|
|
984
|
+
driver,
|
|
985
|
+
context,
|
|
986
|
+
_files.build_upload_endpoint_exchange(
|
|
987
|
+
context.canonical_origin,
|
|
988
|
+
context.user_agent,
|
|
989
|
+
_protocol.classify_endpoint('finalize'),
|
|
990
|
+
_files.COMPLETE_MULTIPART,
|
|
991
|
+
fields,
|
|
992
|
+
),
|
|
993
|
+
lambda text, _headers: _files.parse_multipart_completion(text),
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
def _upload_for(
|
|
997
|
+
self,
|
|
998
|
+
driver: Driver,
|
|
999
|
+
context: RequestContext,
|
|
1000
|
+
prepared: _files.PreparedUpload,
|
|
1001
|
+
) -> int:
|
|
1002
|
+
"""Execute one prepared upload through a captured driver lease."""
|
|
1003
|
+
arguments = prepared.options.arguments
|
|
1004
|
+
capabilities = self._ensure_readiness(context, driver)
|
|
1005
|
+
upload_path = _files.choose_upload_path(
|
|
1006
|
+
capabilities.server_info,
|
|
1007
|
+
arguments.entity_type,
|
|
1008
|
+
arguments.field_name,
|
|
1009
|
+
prepared.file_size,
|
|
1010
|
+
)
|
|
1011
|
+
if upload_path == 'form':
|
|
1012
|
+
return self._upload_form(driver, context, prepared)
|
|
1013
|
+
multipart = upload_path == 'multipart'
|
|
1014
|
+
upload_info = self._upload_allocation(driver, context, prepared, multipart)
|
|
1015
|
+
if multipart:
|
|
1016
|
+
self._upload_multipart(driver, context, prepared, upload_info)
|
|
1017
|
+
else:
|
|
1018
|
+
self._upload_direct(driver, context, prepared, upload_info)
|
|
1019
|
+
return self._upload_link(driver, context, prepared, upload_info)
|
|
1020
|
+
|
|
1021
|
+
def _upload(self, options: _files.UploadOptions) -> int:
|
|
1022
|
+
"""Execute one evidence-backed upload workflow."""
|
|
1023
|
+
prepared = _files.prepare_upload(options)
|
|
1024
|
+
context = config_module.snapshot(self._config)
|
|
1025
|
+
require_secure_rpc(context, include_auth=True)
|
|
1026
|
+
with self._driver_lease(context) as driver:
|
|
1027
|
+
return self._upload_for(driver, context, prepared)
|
|
1028
|
+
|
|
1029
|
+
def _upload_thumbnail_for(
|
|
1030
|
+
self,
|
|
1031
|
+
driver: Driver,
|
|
1032
|
+
context: RequestContext,
|
|
1033
|
+
prepared: _files.PreparedUpload,
|
|
1034
|
+
) -> int:
|
|
1035
|
+
"""Execute the exact direct-storage thumbnail workflow."""
|
|
1036
|
+
self._ensure_readiness(context, driver)
|
|
1037
|
+
upload_info = self._upload_allocation(driver, context, prepared, multipart=False)
|
|
1038
|
+
self._upload_direct(driver, context, prepared, upload_info)
|
|
1039
|
+
return self._upload_link(driver, context, prepared, upload_info)
|
|
1040
|
+
|
|
1041
|
+
@overload
|
|
1042
|
+
def activity_stream_read(self, entity_type: Never) -> dict[str, object]: ...
|
|
1043
|
+
|
|
1044
|
+
@overload
|
|
1045
|
+
def activity_stream_read(
|
|
1046
|
+
self,
|
|
1047
|
+
entity_type: str,
|
|
1048
|
+
entity_id: int,
|
|
1049
|
+
entity_fields: dict[str, list[str]] | None = None,
|
|
1050
|
+
min_id: int | None = None,
|
|
1051
|
+
max_id: int | None = None,
|
|
1052
|
+
limit: int | None = None,
|
|
1053
|
+
) -> dict[str, object]: ...
|
|
1054
|
+
|
|
1055
|
+
@_methods.expose_activity_stream_read_signature
|
|
1056
|
+
def activity_stream_read(self, *args: object, **kwargs: object) -> dict[str, object]:
|
|
1057
|
+
"""Read the activity stream for one entity.
|
|
1058
|
+
|
|
1059
|
+
Args:
|
|
1060
|
+
*args: Positional arguments from the documented activity-stream
|
|
1061
|
+
signature.
|
|
1062
|
+
**kwargs: Keyword arguments from the documented activity-stream
|
|
1063
|
+
signature.
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
Activity stream data returned by the server.
|
|
1067
|
+
|
|
1068
|
+
Raises:
|
|
1069
|
+
TypeError: If arguments do not match the documented signature.
|
|
1070
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1071
|
+
or if the server result has the wrong type.
|
|
1072
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1073
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1074
|
+
policy.
|
|
1075
|
+
"""
|
|
1076
|
+
options = _methods.bind_activity_stream_read(args, kwargs, 'Shotgun')
|
|
1077
|
+
return _crud.require_dictionary_result(
|
|
1078
|
+
self._call(
|
|
1079
|
+
'activity_stream',
|
|
1080
|
+
_methods.build_activity_stream_read_params(options),
|
|
1081
|
+
invocation_options=_InvocationOptions(logical_method_name='activity_stream_read'),
|
|
1082
|
+
),
|
|
1083
|
+
'activity_stream_read',
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
def add_user_agent(self, agent: str) -> None:
|
|
1087
|
+
"""Append a user-agent fragment.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
agent: User-agent fragment visible to subsequent requests.
|
|
1091
|
+
"""
|
|
1092
|
+
self._config.append_user_agent(agent)
|
|
1093
|
+
|
|
1094
|
+
def authenticate_human_user(
|
|
1095
|
+
self,
|
|
1096
|
+
user_login: str,
|
|
1097
|
+
user_password: str,
|
|
1098
|
+
auth_token: str | None = None,
|
|
1099
|
+
) -> dict[str, object] | None:
|
|
1100
|
+
"""Authenticate a human user without changing the client's own auth.
|
|
1101
|
+
|
|
1102
|
+
Args:
|
|
1103
|
+
user_login: Login of the user to authenticate.
|
|
1104
|
+
user_password: Password for `user_login`.
|
|
1105
|
+
auth_token: Optional two-factor authentication token.
|
|
1106
|
+
|
|
1107
|
+
Returns:
|
|
1108
|
+
The matching `HumanUser` entity, or `None` when no active user
|
|
1109
|
+
matches or the server rejects the credentials.
|
|
1110
|
+
|
|
1111
|
+
Raises:
|
|
1112
|
+
MissingTwoFactorAuthenticationFault: If two-factor authentication
|
|
1113
|
+
is required but missing.
|
|
1114
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1115
|
+
or if the server result has the wrong type.
|
|
1116
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1117
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1118
|
+
policy.
|
|
1119
|
+
"""
|
|
1120
|
+
plan = _methods.build_authentication_plan(
|
|
1121
|
+
user_login,
|
|
1122
|
+
user_password,
|
|
1123
|
+
auth_token,
|
|
1124
|
+
)
|
|
1125
|
+
try:
|
|
1126
|
+
raw_result = self._call(
|
|
1127
|
+
'read',
|
|
1128
|
+
plan.params,
|
|
1129
|
+
invocation_options=_InvocationOptions(
|
|
1130
|
+
logical_method_name='authenticate_human_user',
|
|
1131
|
+
auth_spec=plan.auth_spec,
|
|
1132
|
+
),
|
|
1133
|
+
)
|
|
1134
|
+
except _errors.AuthenticationFault:
|
|
1135
|
+
return None
|
|
1136
|
+
return _methods.require_human_authentication_result(raw_result)
|
|
1137
|
+
|
|
1138
|
+
@property
|
|
1139
|
+
def base_url(self) -> str:
|
|
1140
|
+
"""Return the constructor's normalized base URL.
|
|
1141
|
+
|
|
1142
|
+
Returns:
|
|
1143
|
+
The site URL without a trailing slash.
|
|
1144
|
+
"""
|
|
1145
|
+
return self._base_url
|
|
1146
|
+
|
|
1147
|
+
def batch(self, requests: list[dict[str, Any]]) -> list[object]:
|
|
1148
|
+
"""Run create, update, and delete requests in one transaction.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
requests: Ordered request dictionaries to validate and send.
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
Results in the same order as the supplied requests.
|
|
1155
|
+
|
|
1156
|
+
Raises:
|
|
1157
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1158
|
+
or if a request is malformed or has an unsupported request
|
|
1159
|
+
type, or if the server result is not a list.
|
|
1160
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1161
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1162
|
+
policy.
|
|
1163
|
+
"""
|
|
1164
|
+
calls = _crud.build_batch_calls(requests)
|
|
1165
|
+
if not calls:
|
|
1166
|
+
return []
|
|
1167
|
+
return _crud.require_list_result(self._call('batch', calls), 'batch')
|
|
1168
|
+
|
|
1169
|
+
@property
|
|
1170
|
+
def client_caps(self) -> ClientCapabilities:
|
|
1171
|
+
"""Return capabilities of this Python client.
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
Local platform capability metadata.
|
|
1175
|
+
"""
|
|
1176
|
+
return self._client_caps
|
|
1177
|
+
|
|
1178
|
+
def _find(self, options: _crud.FindOptions) -> list[dict[str, object]]:
|
|
1179
|
+
"""Run one paged read operation from one immutable context."""
|
|
1180
|
+
prepared = _crud.prepare_find(options)
|
|
1181
|
+
context = config_module.snapshot(self._config)
|
|
1182
|
+
require_secure_rpc(context, include_auth=True)
|
|
1183
|
+
records: list[dict[str, object]] = []
|
|
1184
|
+
|
|
1185
|
+
with self._driver_lease(context) as driver:
|
|
1186
|
+
capabilities = self._ensure_readiness(context, driver)
|
|
1187
|
+
operation_context = dataclasses.replace(
|
|
1188
|
+
context,
|
|
1189
|
+
records_per_page=server_page_limit(capabilities.server_info),
|
|
1190
|
+
)
|
|
1191
|
+
plan = _crud.build_find_plan(
|
|
1192
|
+
prepared,
|
|
1193
|
+
operation_context.records_per_page,
|
|
1194
|
+
)
|
|
1195
|
+
if not plan.multi_page:
|
|
1196
|
+
operation_context = dataclasses.replace(
|
|
1197
|
+
operation_context,
|
|
1198
|
+
page_prefetch=0,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
def build_page_params(page_number: int) -> object:
|
|
1202
|
+
return _crud.build_page_params(
|
|
1203
|
+
plan,
|
|
1204
|
+
page_number,
|
|
1205
|
+
operation_context.convert_datetimes_to_utc,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
pages = driver.call_pages(
|
|
1209
|
+
build_page_params,
|
|
1210
|
+
plan.paging['current_page'],
|
|
1211
|
+
'read',
|
|
1212
|
+
operation_context,
|
|
1213
|
+
)
|
|
1214
|
+
try:
|
|
1215
|
+
for raw_result in pages:
|
|
1216
|
+
page = _crud.validate_read_page(
|
|
1217
|
+
raw_result,
|
|
1218
|
+
convert_utc=operation_context.convert_datetimes_to_utc,
|
|
1219
|
+
records_count=len(records),
|
|
1220
|
+
plan=plan,
|
|
1221
|
+
)
|
|
1222
|
+
records.extend(page.entities)
|
|
1223
|
+
if page.stop:
|
|
1224
|
+
break
|
|
1225
|
+
finally:
|
|
1226
|
+
pages.close()
|
|
1227
|
+
return _crud.finalize_find(
|
|
1228
|
+
records,
|
|
1229
|
+
options,
|
|
1230
|
+
plan,
|
|
1231
|
+
self.client_caps.local_path_field,
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
def close(self) -> None:
|
|
1235
|
+
"""Retire known and current transports after in-flight work releases them."""
|
|
1236
|
+
close_after_unlock: list[Transport] = []
|
|
1237
|
+
with self._transport_lock:
|
|
1238
|
+
advance_transport_generation(self._config)
|
|
1239
|
+
if self._known_transport is not None and self._known_transport is not self._transport:
|
|
1240
|
+
retired = retire_transport(
|
|
1241
|
+
self._lease_counts, self._retired_transports, self._known_transport
|
|
1242
|
+
)
|
|
1243
|
+
if retired is not None:
|
|
1244
|
+
close_after_unlock.append(retired)
|
|
1245
|
+
transport = self._transport
|
|
1246
|
+
self._driver = None
|
|
1247
|
+
self._driver_generation = None
|
|
1248
|
+
self._known_transport = None
|
|
1249
|
+
self._transport = None
|
|
1250
|
+
self._transport_owned = False
|
|
1251
|
+
if transport is not None:
|
|
1252
|
+
retired = retire_transport(self._lease_counts, self._retired_transports, transport)
|
|
1253
|
+
if retired is not None:
|
|
1254
|
+
close_after_unlock.append(retired)
|
|
1255
|
+
pending_closes = iter(close_after_unlock)
|
|
1256
|
+
try:
|
|
1257
|
+
for retired in pending_closes:
|
|
1258
|
+
retired.close()
|
|
1259
|
+
except BaseException:
|
|
1260
|
+
for retired in pending_closes:
|
|
1261
|
+
with suppress(BaseException):
|
|
1262
|
+
retired.close()
|
|
1263
|
+
raise
|
|
1264
|
+
with self._readiness:
|
|
1265
|
+
current_key = config_module.readiness_identity(self._config)
|
|
1266
|
+
current = self._ready_caps.get(current_key) if current_key is not None else None
|
|
1267
|
+
self._ready_caps.clear()
|
|
1268
|
+
if current_key is not None and current is not None:
|
|
1269
|
+
self._ready_caps[current_key] = current
|
|
1270
|
+
|
|
1271
|
+
@property
|
|
1272
|
+
def config(self) -> Config:
|
|
1273
|
+
"""Return the writable client configuration.
|
|
1274
|
+
|
|
1275
|
+
Returns:
|
|
1276
|
+
The locked client configuration object.
|
|
1277
|
+
"""
|
|
1278
|
+
return self._config
|
|
1279
|
+
|
|
1280
|
+
def connect(self) -> None:
|
|
1281
|
+
"""Validate readiness for the configured origin.
|
|
1282
|
+
|
|
1283
|
+
Raises:
|
|
1284
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs.
|
|
1285
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1286
|
+
ValueError: If server parameters are missing or the configured
|
|
1287
|
+
origin lacks a valid host.
|
|
1288
|
+
"""
|
|
1289
|
+
context = config_module.snapshot(self._config)
|
|
1290
|
+
with self._driver_lease(context) as driver:
|
|
1291
|
+
self._ensure_readiness(context, driver)
|
|
1292
|
+
|
|
1293
|
+
def create(
|
|
1294
|
+
self,
|
|
1295
|
+
entity_type: str,
|
|
1296
|
+
data: dict[str, Any],
|
|
1297
|
+
return_fields: list[str] | None = None,
|
|
1298
|
+
) -> dict[str, object]:
|
|
1299
|
+
"""Create an entity with the supplied field values.
|
|
1300
|
+
|
|
1301
|
+
Args:
|
|
1302
|
+
entity_type: Entity type to create.
|
|
1303
|
+
data: Field values for the new entity.
|
|
1304
|
+
return_fields: Fields to return in addition to the entity identity.
|
|
1305
|
+
|
|
1306
|
+
Returns:
|
|
1307
|
+
The created entity data returned by the server.
|
|
1308
|
+
|
|
1309
|
+
Raises:
|
|
1310
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1311
|
+
or if `data`, its field names, `return_fields`, or the server
|
|
1312
|
+
result is invalid.
|
|
1313
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1314
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1315
|
+
policy.
|
|
1316
|
+
"""
|
|
1317
|
+
params = _crud.build_create_params(entity_type, data, return_fields)
|
|
1318
|
+
return _crud.require_dictionary_result(self._call('create', params), 'create')
|
|
1319
|
+
|
|
1320
|
+
def delete(self, entity_type: str, entity_id: int) -> bool:
|
|
1321
|
+
"""Retire an entity.
|
|
1322
|
+
|
|
1323
|
+
Args:
|
|
1324
|
+
entity_type: Entity type to retire.
|
|
1325
|
+
entity_id: Numeric entity identifier.
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
Whether the server retired the entity.
|
|
1329
|
+
|
|
1330
|
+
Raises:
|
|
1331
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1332
|
+
or if the server result is not a boolean.
|
|
1333
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1334
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1335
|
+
policy.
|
|
1336
|
+
"""
|
|
1337
|
+
return _crud.require_boolean_result(
|
|
1338
|
+
self._call('delete', _crud.build_entity_action_params(entity_type, entity_id)),
|
|
1339
|
+
'delete',
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
@overload
|
|
1343
|
+
def download_attachment(
|
|
1344
|
+
self,
|
|
1345
|
+
attachment: int | Mapping[str, object] | None,
|
|
1346
|
+
file_path: str | None = None,
|
|
1347
|
+
attachment_id: int | None = None,
|
|
1348
|
+
) -> bytes | str | None: ...
|
|
1349
|
+
|
|
1350
|
+
@overload
|
|
1351
|
+
def download_attachment(
|
|
1352
|
+
self,
|
|
1353
|
+
*,
|
|
1354
|
+
file_path: str | None = None,
|
|
1355
|
+
attachment_id: int,
|
|
1356
|
+
) -> bytes | str | None: ...
|
|
1357
|
+
|
|
1358
|
+
def download_attachment(
|
|
1359
|
+
self,
|
|
1360
|
+
attachment: int | Mapping[str, object] | None = False,
|
|
1361
|
+
file_path: str | None = None,
|
|
1362
|
+
attachment_id: int | None = None,
|
|
1363
|
+
) -> bytes | str | None:
|
|
1364
|
+
"""Download an attachment to memory or an atomic destination path.
|
|
1365
|
+
|
|
1366
|
+
Args:
|
|
1367
|
+
attachment: Attachment ID, Attachment dictionary, URL dictionary,
|
|
1368
|
+
or `None`.
|
|
1369
|
+
file_path: Optional destination path.
|
|
1370
|
+
attachment_id: Legacy attachment ID used when `attachment` is
|
|
1371
|
+
omitted.
|
|
1372
|
+
|
|
1373
|
+
Returns:
|
|
1374
|
+
Downloaded bytes, the supplied destination path, or `None`.
|
|
1375
|
+
|
|
1376
|
+
Raises:
|
|
1377
|
+
TypeError: If no attachment is given or the reference is a string.
|
|
1378
|
+
ValueError: If an entity dictionary references a different type.
|
|
1379
|
+
ShotgunError: If another attachment reference is unsupported or a
|
|
1380
|
+
setup RPC reports a fault; the fault keeps its specific type.
|
|
1381
|
+
ShotgunFileDownloadError: If the resolved URL is invalid or setup,
|
|
1382
|
+
transfer, or destination I/O fails.
|
|
1383
|
+
ValueError: If server settings are incomplete or authenticated HTTP
|
|
1384
|
+
violates the configured origin security policy.
|
|
1385
|
+
"""
|
|
1386
|
+
if attachment is False:
|
|
1387
|
+
if attachment_id is None:
|
|
1388
|
+
raise TypeError('attachment or attachment_id is required')
|
|
1389
|
+
attachment = attachment_id
|
|
1390
|
+
context = config_module.snapshot(self._config)
|
|
1391
|
+
if context.canonical_origin is None or context.site_url is None:
|
|
1392
|
+
raise ValueError('server parameters must be configured before download I/O')
|
|
1393
|
+
url = _files.resolve_attachment_url(attachment, context.site_url)
|
|
1394
|
+
if url is None:
|
|
1395
|
+
return None
|
|
1396
|
+
_files.validate_download_url(url)
|
|
1397
|
+
origin = _files.canonical_origin(context.canonical_origin)
|
|
1398
|
+
use_session_cookie = _files.needs_session_cookie(url, origin)
|
|
1399
|
+
if use_session_cookie:
|
|
1400
|
+
require_secure_rpc(context, include_auth=True)
|
|
1401
|
+
operation = _protocol.classify_endpoint('download')
|
|
1402
|
+
|
|
1403
|
+
try:
|
|
1404
|
+
with self._driver_lease(context) as driver:
|
|
1405
|
+
headers = {'user-agent': context.user_agent}
|
|
1406
|
+
if use_session_cookie:
|
|
1407
|
+
if context.session_token is None:
|
|
1408
|
+
token = self._get_session_token_for(context, driver)
|
|
1409
|
+
else:
|
|
1410
|
+
self._ensure_readiness(context, driver)
|
|
1411
|
+
token = context.session_token
|
|
1412
|
+
headers['cookie'] = f'_session_id={token}'
|
|
1413
|
+
exchange = _DownloadExchange(
|
|
1414
|
+
context=context,
|
|
1415
|
+
file_path=file_path,
|
|
1416
|
+
headers=headers,
|
|
1417
|
+
url=url,
|
|
1418
|
+
)
|
|
1419
|
+
return exchange._execute(driver, operation) # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
|
|
1420
|
+
except _errors.ShotgunFileDownloadError:
|
|
1421
|
+
raise
|
|
1422
|
+
except (TransportError, _errors.ProtocolError, OSError):
|
|
1423
|
+
raise _errors.ShotgunFileDownloadError('attachment download failed') from None
|
|
1424
|
+
|
|
1425
|
+
@overload
|
|
1426
|
+
def export_page(
|
|
1427
|
+
self,
|
|
1428
|
+
page_id: int,
|
|
1429
|
+
page_format: str,
|
|
1430
|
+
/,
|
|
1431
|
+
layout_name: str | None = None,
|
|
1432
|
+
) -> str: ...
|
|
1433
|
+
|
|
1434
|
+
@overload
|
|
1435
|
+
def export_page(
|
|
1436
|
+
self,
|
|
1437
|
+
page_id: int,
|
|
1438
|
+
**kwargs: Unpack[_methods.ExportPageKeywordArguments],
|
|
1439
|
+
) -> str: ...
|
|
1440
|
+
|
|
1441
|
+
@_methods.expose_export_page_signature
|
|
1442
|
+
def export_page(self, *args: object, **kwargs: object) -> str:
|
|
1443
|
+
"""Export a page using the documented arguments.
|
|
1444
|
+
|
|
1445
|
+
Args:
|
|
1446
|
+
*args: Positional values from the documented `export_page`
|
|
1447
|
+
signature.
|
|
1448
|
+
**kwargs: Keyword values from the documented `export_page`
|
|
1449
|
+
signature.
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
Exported page text returned by the server.
|
|
1453
|
+
|
|
1454
|
+
Raises:
|
|
1455
|
+
TypeError: If arguments do not match the documented signature.
|
|
1456
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1457
|
+
or if an argument or the server result has the wrong type.
|
|
1458
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1459
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1460
|
+
policy.
|
|
1461
|
+
"""
|
|
1462
|
+
options = _methods.bind_export_page(args, kwargs, 'Shotgun')
|
|
1463
|
+
params = _methods.build_export_page_params(options)
|
|
1464
|
+
return _methods.require_string_result(self._call('export_page', params), 'export_page')
|
|
1465
|
+
|
|
1466
|
+
@overload
|
|
1467
|
+
def find(self, entity_type: Never) -> list[dict[str, object]]: ...
|
|
1468
|
+
|
|
1469
|
+
@overload
|
|
1470
|
+
def find(
|
|
1471
|
+
self,
|
|
1472
|
+
entity_type: str,
|
|
1473
|
+
filters: list[Any] | tuple[Any, ...] | dict[str, Any],
|
|
1474
|
+
fields: list[str] | None = None,
|
|
1475
|
+
order: list[dict[str, str]] | None = None,
|
|
1476
|
+
filter_operator: str | None = None,
|
|
1477
|
+
limit: int = 0,
|
|
1478
|
+
retired_only: bool = False,
|
|
1479
|
+
page: int = 0,
|
|
1480
|
+
include_archived_projects: bool = True,
|
|
1481
|
+
additional_filter_presets: object = None,
|
|
1482
|
+
) -> list[dict[str, object]]: ...
|
|
1483
|
+
|
|
1484
|
+
@_crud.expose_find_signature
|
|
1485
|
+
def find(self, *args: object, **kwargs: object) -> list[dict[str, object]]:
|
|
1486
|
+
"""Find entities matching the supplied filters.
|
|
1487
|
+
|
|
1488
|
+
Args:
|
|
1489
|
+
*args: Positional values from the documented `find` signature.
|
|
1490
|
+
**kwargs: Keyword values from the documented `find` signature.
|
|
1491
|
+
|
|
1492
|
+
Returns:
|
|
1493
|
+
Matching entities in server order.
|
|
1494
|
+
|
|
1495
|
+
Raises:
|
|
1496
|
+
TypeError: If arguments do not match the documented signature.
|
|
1497
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1498
|
+
or if filters, paging, ordering, or a server result is invalid.
|
|
1499
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1500
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1501
|
+
policy.
|
|
1502
|
+
"""
|
|
1503
|
+
return self._find(_crud.bind_find(args, kwargs, 'Shotgun'))
|
|
1504
|
+
|
|
1505
|
+
@overload
|
|
1506
|
+
def find_one(self, entity_type: Never) -> dict[str, object] | None: ...
|
|
1507
|
+
|
|
1508
|
+
@overload
|
|
1509
|
+
def find_one(
|
|
1510
|
+
self,
|
|
1511
|
+
entity_type: str,
|
|
1512
|
+
filters: list[Any] | tuple[Any, ...] | dict[str, Any],
|
|
1513
|
+
fields: list[str] | None = None,
|
|
1514
|
+
order: list[dict[str, str]] | None = None,
|
|
1515
|
+
filter_operator: str | None = None,
|
|
1516
|
+
retired_only: bool = False,
|
|
1517
|
+
include_archived_projects: bool = True,
|
|
1518
|
+
additional_filter_presets: object = None,
|
|
1519
|
+
) -> dict[str, object] | None: ...
|
|
1520
|
+
|
|
1521
|
+
@_crud.expose_find_one_signature
|
|
1522
|
+
def find_one(self, *args: object, **kwargs: object) -> dict[str, object] | None:
|
|
1523
|
+
"""Return the first entity matching the supplied filters.
|
|
1524
|
+
|
|
1525
|
+
Args:
|
|
1526
|
+
*args: Positional values from the documented `find_one` signature.
|
|
1527
|
+
**kwargs: Keyword values from the documented `find_one` signature.
|
|
1528
|
+
|
|
1529
|
+
Returns:
|
|
1530
|
+
The first matching entity, or `None` when no entity matches.
|
|
1531
|
+
|
|
1532
|
+
Raises:
|
|
1533
|
+
TypeError: If arguments do not match the documented signature.
|
|
1534
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1535
|
+
or if filters, paging, ordering, or a server result is invalid.
|
|
1536
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1537
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1538
|
+
policy.
|
|
1539
|
+
"""
|
|
1540
|
+
options = _crud.bind_find_one(args, kwargs, 'Shotgun')
|
|
1541
|
+
return _crud.first_result(self._find(_crud.find_options_from_one(options)))
|
|
1542
|
+
|
|
1543
|
+
def follow(self, user: dict[str, Any], entity: dict[str, Any]) -> dict[str, object]:
|
|
1544
|
+
"""Make a user follow an entity.
|
|
1545
|
+
|
|
1546
|
+
Args:
|
|
1547
|
+
user: User entity that will follow the target.
|
|
1548
|
+
entity: Entity to follow.
|
|
1549
|
+
|
|
1550
|
+
Returns:
|
|
1551
|
+
Follow result returned by the server.
|
|
1552
|
+
|
|
1553
|
+
Raises:
|
|
1554
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1555
|
+
or if the server result has the wrong type.
|
|
1556
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1557
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1558
|
+
policy.
|
|
1559
|
+
"""
|
|
1560
|
+
params = _methods.build_follow_params(user, entity)
|
|
1561
|
+
return _crud.require_dictionary_result(self._call('follow', params), 'follow')
|
|
1562
|
+
|
|
1563
|
+
def followers(self, entity: dict[str, Any]) -> list[dict[str, object]]:
|
|
1564
|
+
"""List users following an entity.
|
|
1565
|
+
|
|
1566
|
+
Args:
|
|
1567
|
+
entity: Entity whose followers to list.
|
|
1568
|
+
|
|
1569
|
+
Returns:
|
|
1570
|
+
Following user entities returned by the server.
|
|
1571
|
+
|
|
1572
|
+
Raises:
|
|
1573
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1574
|
+
or if the server result has the wrong type.
|
|
1575
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1576
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1577
|
+
policy.
|
|
1578
|
+
"""
|
|
1579
|
+
params = _methods.build_followers_params(entity)
|
|
1580
|
+
return _methods.require_dictionary_list_result(
|
|
1581
|
+
self._call('followers', params),
|
|
1582
|
+
'followers',
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
def following(
|
|
1586
|
+
self,
|
|
1587
|
+
user: dict[str, Any],
|
|
1588
|
+
project: dict[str, Any] | None = None,
|
|
1589
|
+
entity_type: str | None = None,
|
|
1590
|
+
) -> list[dict[str, object]]:
|
|
1591
|
+
"""List entities a user follows, optionally narrowed by project and type.
|
|
1592
|
+
|
|
1593
|
+
Args:
|
|
1594
|
+
user: User entity whose followed entities to list.
|
|
1595
|
+
project: Optional project entity restriction.
|
|
1596
|
+
entity_type: Optional entity-type restriction.
|
|
1597
|
+
|
|
1598
|
+
Returns:
|
|
1599
|
+
Followed entities returned by the server.
|
|
1600
|
+
|
|
1601
|
+
Raises:
|
|
1602
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1603
|
+
or if the server result has the wrong type.
|
|
1604
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1605
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1606
|
+
policy.
|
|
1607
|
+
"""
|
|
1608
|
+
params = _methods.build_following_params(user, project, entity_type)
|
|
1609
|
+
return _methods.require_dictionary_list_result(
|
|
1610
|
+
self._call('following', params),
|
|
1611
|
+
'following',
|
|
1612
|
+
)
|
|
1613
|
+
|
|
1614
|
+
def get_attachment_download_url(
|
|
1615
|
+
self, attachment: int | Mapping[str, object] | None
|
|
1616
|
+
) -> str | None:
|
|
1617
|
+
"""Resolve an attachment reference to its download URL.
|
|
1618
|
+
|
|
1619
|
+
Args:
|
|
1620
|
+
attachment: Attachment ID, Attachment dictionary, URL dictionary,
|
|
1621
|
+
or `None`.
|
|
1622
|
+
|
|
1623
|
+
Returns:
|
|
1624
|
+
The supplied URL, a site attachment URL, or `None`.
|
|
1625
|
+
|
|
1626
|
+
Raises:
|
|
1627
|
+
TypeError: If the attachment reference is a string.
|
|
1628
|
+
ValueError: If an entity dictionary references a different type or
|
|
1629
|
+
server parameters have not been configured.
|
|
1630
|
+
ShotgunError: If the attachment reference is unsupported.
|
|
1631
|
+
"""
|
|
1632
|
+
if attachment is None:
|
|
1633
|
+
return None
|
|
1634
|
+
if isinstance(attachment, Mapping):
|
|
1635
|
+
mapping = cast('Mapping[object, object]', attachment)
|
|
1636
|
+
url = mapping.get('url')
|
|
1637
|
+
if isinstance(url, str):
|
|
1638
|
+
return url
|
|
1639
|
+
context = config_module.snapshot(self._config)
|
|
1640
|
+
if context.site_url is None:
|
|
1641
|
+
raise ValueError('server parameters must be configured before URL generation')
|
|
1642
|
+
return _files.resolve_attachment_url(cast('object', attachment), context.site_url)
|
|
1643
|
+
|
|
1644
|
+
def get_auth_cookie_handler(self) -> urllib.request.HTTPCookieProcessor:
|
|
1645
|
+
"""Return a session-cookie handler bound to the captured origin.
|
|
1646
|
+
|
|
1647
|
+
Returns:
|
|
1648
|
+
Stdlib handler containing one exact-origin session cookie.
|
|
1649
|
+
|
|
1650
|
+
Raises:
|
|
1651
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1652
|
+
or if token exchange returns an invalid result.
|
|
1653
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1654
|
+
ValueError: If server parameters have not been configured or
|
|
1655
|
+
authenticated HTTP violates the configured origin policy.
|
|
1656
|
+
"""
|
|
1657
|
+
context = config_module.snapshot(self._config)
|
|
1658
|
+
if context.canonical_origin is None:
|
|
1659
|
+
raise ValueError('server parameters must be configured before cookie creation')
|
|
1660
|
+
origin = _files.canonical_origin(context.canonical_origin)
|
|
1661
|
+
if context.session_token is None:
|
|
1662
|
+
require_secure_rpc(context, include_auth=True)
|
|
1663
|
+
with self._driver_lease(context) as driver:
|
|
1664
|
+
token = self._get_session_token_for(context, driver)
|
|
1665
|
+
return _files.build_auth_cookie_handler(token, origin)
|
|
1666
|
+
|
|
1667
|
+
def get_session_token(self) -> str:
|
|
1668
|
+
"""Return or obtain a session token for the current credentials.
|
|
1669
|
+
|
|
1670
|
+
Returns:
|
|
1671
|
+
The existing or newly exchanged session token.
|
|
1672
|
+
|
|
1673
|
+
Raises:
|
|
1674
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1675
|
+
or if the RPC result lacks the expected session ID.
|
|
1676
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1677
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1678
|
+
policy.
|
|
1679
|
+
"""
|
|
1680
|
+
context = config_module.snapshot(self._config)
|
|
1681
|
+
if context.session_token is not None:
|
|
1682
|
+
return context.session_token
|
|
1683
|
+
require_secure_rpc(context, include_auth=True)
|
|
1684
|
+
with self._driver_lease(context) as driver:
|
|
1685
|
+
return self._get_session_token_for(context, driver)
|
|
1686
|
+
|
|
1687
|
+
def _initialize(self, options: ConstructorOptions) -> None:
|
|
1688
|
+
"""Initialize client state from validated constructor options."""
|
|
1689
|
+
self._base_url = options.base_url.rstrip('/')
|
|
1690
|
+
self._ca_certs = options.ca_certs or os.environ.get('SHOTGUN_API_CACERTS') or None
|
|
1691
|
+
self._client_caps = ClientCapabilities()
|
|
1692
|
+
self._config = build_config(options)
|
|
1693
|
+
|
|
1694
|
+
self._driver: Driver | None = None
|
|
1695
|
+
self._driver_generation: int | None = None
|
|
1696
|
+
self._known_transport: Transport | None = None
|
|
1697
|
+
self._lease_counts: dict[int, int] = {}
|
|
1698
|
+
self._retired_transports: dict[int, Transport] = {}
|
|
1699
|
+
self._transport: Transport | None = None
|
|
1700
|
+
self._transport_lock = threading.Lock()
|
|
1701
|
+
self._transport_owned = False
|
|
1702
|
+
|
|
1703
|
+
self._readiness = threading.Condition()
|
|
1704
|
+
self._readiness_in_progress: set[_ReadinessKey] = set()
|
|
1705
|
+
self._readiness_owner: dict[_ReadinessKey, int] = {}
|
|
1706
|
+
self._ready_caps: dict[_ReadinessKey, ServerCapabilities] = {}
|
|
1707
|
+
|
|
1708
|
+
if options.connect:
|
|
1709
|
+
self.connect()
|
|
1710
|
+
if options.auth_token is not None:
|
|
1711
|
+
self.get_session_token()
|
|
1712
|
+
|
|
1713
|
+
def info(self) -> dict[str, object]:
|
|
1714
|
+
"""Fetch and publish unauthenticated server metadata.
|
|
1715
|
+
|
|
1716
|
+
Returns:
|
|
1717
|
+
Raw server metadata after version-floor validation.
|
|
1718
|
+
|
|
1719
|
+
Raises:
|
|
1720
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs.
|
|
1721
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1722
|
+
ValueError: If server parameters are missing or the configured
|
|
1723
|
+
origin lacks a valid host.
|
|
1724
|
+
"""
|
|
1725
|
+
return cast(
|
|
1726
|
+
'dict[str, object]',
|
|
1727
|
+
self._call('info', None, _InvocationOptions(include_auth=False)),
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
def nav_expand(
|
|
1731
|
+
self,
|
|
1732
|
+
path: str,
|
|
1733
|
+
seed_entity_field: str | None = None,
|
|
1734
|
+
entity_fields: dict[str, list[str]] | None = None,
|
|
1735
|
+
) -> dict[str, object]:
|
|
1736
|
+
"""Expand one level of the navigation hierarchy at `path`.
|
|
1737
|
+
|
|
1738
|
+
Args:
|
|
1739
|
+
path: Navigation path to expand.
|
|
1740
|
+
seed_entity_field: Optional field used to seed the navigation path.
|
|
1741
|
+
entity_fields: Optional fields to return for linked entity types.
|
|
1742
|
+
|
|
1743
|
+
Returns:
|
|
1744
|
+
Expanded navigation data returned by the server.
|
|
1745
|
+
|
|
1746
|
+
Raises:
|
|
1747
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1748
|
+
or if the server result has the wrong type.
|
|
1749
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1750
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1751
|
+
policy.
|
|
1752
|
+
"""
|
|
1753
|
+
params = _methods.build_nav_expand_params(
|
|
1754
|
+
path,
|
|
1755
|
+
seed_entity_field,
|
|
1756
|
+
entity_fields,
|
|
1757
|
+
)
|
|
1758
|
+
return _crud.require_dictionary_result(
|
|
1759
|
+
self._call('nav_expand', params),
|
|
1760
|
+
'nav_expand',
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
def nav_search_entity(
|
|
1764
|
+
self,
|
|
1765
|
+
root_path: str,
|
|
1766
|
+
entity: dict[str, Any],
|
|
1767
|
+
seed_entity_field: str | None = None,
|
|
1768
|
+
) -> list[dict[str, object]]:
|
|
1769
|
+
"""Search the navigation hierarchy under `root_path` for an entity.
|
|
1770
|
+
|
|
1771
|
+
Args:
|
|
1772
|
+
root_path: Navigation path under which to search.
|
|
1773
|
+
entity: Entity to find in the navigation hierarchy.
|
|
1774
|
+
seed_entity_field: Optional field used to seed the navigation path.
|
|
1775
|
+
|
|
1776
|
+
Returns:
|
|
1777
|
+
Matching navigation data returned by the server.
|
|
1778
|
+
|
|
1779
|
+
Raises:
|
|
1780
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1781
|
+
or if the server result has the wrong type.
|
|
1782
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1783
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1784
|
+
policy.
|
|
1785
|
+
"""
|
|
1786
|
+
params = _methods.build_nav_search_entity_params(
|
|
1787
|
+
root_path,
|
|
1788
|
+
entity,
|
|
1789
|
+
seed_entity_field,
|
|
1790
|
+
)
|
|
1791
|
+
return _methods.require_dictionary_list_result(
|
|
1792
|
+
self._call('nav_search_entity', params),
|
|
1793
|
+
'nav_search_entity',
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
def nav_search_string(
|
|
1797
|
+
self,
|
|
1798
|
+
root_path: str,
|
|
1799
|
+
search_string: str,
|
|
1800
|
+
seed_entity_field: str | None = None,
|
|
1801
|
+
) -> list[dict[str, object]]:
|
|
1802
|
+
"""Search the navigation hierarchy under `root_path` by string.
|
|
1803
|
+
|
|
1804
|
+
Args:
|
|
1805
|
+
root_path: Navigation path under which to search.
|
|
1806
|
+
search_string: String to match in the navigation hierarchy.
|
|
1807
|
+
seed_entity_field: Optional field used to seed the navigation path.
|
|
1808
|
+
|
|
1809
|
+
Returns:
|
|
1810
|
+
Matching navigation data returned by the server.
|
|
1811
|
+
|
|
1812
|
+
Raises:
|
|
1813
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1814
|
+
or if the server result has the wrong type.
|
|
1815
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1816
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1817
|
+
policy.
|
|
1818
|
+
"""
|
|
1819
|
+
params = _methods.build_nav_search_string_params(
|
|
1820
|
+
root_path,
|
|
1821
|
+
search_string,
|
|
1822
|
+
seed_entity_field,
|
|
1823
|
+
)
|
|
1824
|
+
return _methods.require_dictionary_list_result(
|
|
1825
|
+
self._call('nav_search_string', params),
|
|
1826
|
+
'nav_search_string',
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
def note_thread_read(
|
|
1830
|
+
self,
|
|
1831
|
+
note_id: int,
|
|
1832
|
+
entity_fields: dict[str, list[str]] | None = None,
|
|
1833
|
+
) -> list[dict[str, object]]:
|
|
1834
|
+
"""Read a note and its replies and attachments as a flat thread.
|
|
1835
|
+
|
|
1836
|
+
Args:
|
|
1837
|
+
note_id: Numeric identifier of the note.
|
|
1838
|
+
entity_fields: Optional fields to return for linked entity types.
|
|
1839
|
+
|
|
1840
|
+
Returns:
|
|
1841
|
+
Note thread records returned by the server.
|
|
1842
|
+
|
|
1843
|
+
Raises:
|
|
1844
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1845
|
+
or if the server result has the wrong type.
|
|
1846
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1847
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1848
|
+
policy.
|
|
1849
|
+
"""
|
|
1850
|
+
params = _methods.build_note_thread_read_params(note_id, entity_fields)
|
|
1851
|
+
return _methods.require_dictionary_list_result(
|
|
1852
|
+
self._call('note_thread_read', params),
|
|
1853
|
+
'note_thread_read',
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
def preferences_read(self, prefs: list[str] | None = None) -> dict[str, object]:
|
|
1857
|
+
"""Read site preferences, optionally restricted by name.
|
|
1858
|
+
|
|
1859
|
+
Args:
|
|
1860
|
+
prefs: Preference names to read, or `None` to read all defaults.
|
|
1861
|
+
|
|
1862
|
+
Returns:
|
|
1863
|
+
Preference values returned by the server.
|
|
1864
|
+
|
|
1865
|
+
Raises:
|
|
1866
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1867
|
+
or if the server result has the wrong type.
|
|
1868
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1869
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1870
|
+
policy.
|
|
1871
|
+
"""
|
|
1872
|
+
return _crud.require_dictionary_result(
|
|
1873
|
+
self._call('preferences_read', _methods.build_preferences_read_params(prefs)),
|
|
1874
|
+
'preferences_read',
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
def reset_user_agent(self) -> None:
|
|
1878
|
+
"""Restore this client's default user-agent fragments."""
|
|
1879
|
+
self._config.reset_user_agent()
|
|
1880
|
+
|
|
1881
|
+
def revive(self, entity_type: str, entity_id: int) -> bool:
|
|
1882
|
+
"""Restore a retired entity.
|
|
1883
|
+
|
|
1884
|
+
Args:
|
|
1885
|
+
entity_type: Entity type to restore.
|
|
1886
|
+
entity_id: Numeric entity identifier.
|
|
1887
|
+
|
|
1888
|
+
Returns:
|
|
1889
|
+
Whether the server restored the entity.
|
|
1890
|
+
|
|
1891
|
+
Raises:
|
|
1892
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1893
|
+
or if the server result is not a boolean.
|
|
1894
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1895
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1896
|
+
policy.
|
|
1897
|
+
"""
|
|
1898
|
+
return _crud.require_boolean_result(
|
|
1899
|
+
self._call('revive', _crud.build_entity_action_params(entity_type, entity_id)),
|
|
1900
|
+
'revive',
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
def schema_entity_read(
|
|
1904
|
+
self,
|
|
1905
|
+
project_entity: dict[str, Any] | None = None,
|
|
1906
|
+
) -> dict[str, object]:
|
|
1907
|
+
"""Return entity-type schema, optionally for one project.
|
|
1908
|
+
|
|
1909
|
+
Args:
|
|
1910
|
+
project_entity: Optional project entity restricting the schema.
|
|
1911
|
+
|
|
1912
|
+
Returns:
|
|
1913
|
+
Entity-type schema data returned by the server.
|
|
1914
|
+
|
|
1915
|
+
Raises:
|
|
1916
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1917
|
+
or if the server result is invalid.
|
|
1918
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1919
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1920
|
+
policy.
|
|
1921
|
+
"""
|
|
1922
|
+
return _crud.require_dictionary_result(
|
|
1923
|
+
self._call(
|
|
1924
|
+
'schema_entity_read',
|
|
1925
|
+
_methods.build_schema_entity_read_params(project_entity),
|
|
1926
|
+
_InvocationOptions(
|
|
1927
|
+
compatibility_warning_codes=_methods.consume_compatibility_warnings(
|
|
1928
|
+
_methods.SCHEMA_ENTITY_READ_WARNINGS,
|
|
1929
|
+
self,
|
|
1930
|
+
),
|
|
1931
|
+
),
|
|
1932
|
+
),
|
|
1933
|
+
'schema_entity_read',
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
def entity_types(self) -> dict[str, object]:
|
|
1937
|
+
"""Return entity schema through the deprecated alias.
|
|
1938
|
+
|
|
1939
|
+
Returns:
|
|
1940
|
+
Entity-type schema data returned by the server.
|
|
1941
|
+
|
|
1942
|
+
Raises:
|
|
1943
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1944
|
+
or if the server result is not a dictionary.
|
|
1945
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1946
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1947
|
+
policy.
|
|
1948
|
+
"""
|
|
1949
|
+
_methods.warn_entity_types_alias()
|
|
1950
|
+
with _methods.entity_types_alias(self):
|
|
1951
|
+
return self.schema_entity_read()
|
|
1952
|
+
|
|
1953
|
+
def schema_field_create(
|
|
1954
|
+
self,
|
|
1955
|
+
entity_type: str,
|
|
1956
|
+
data_type: str,
|
|
1957
|
+
display_name: str,
|
|
1958
|
+
properties: dict[str, Any] | None = None,
|
|
1959
|
+
) -> str:
|
|
1960
|
+
"""Create a field and return its internal name.
|
|
1961
|
+
|
|
1962
|
+
Args:
|
|
1963
|
+
entity_type: Entity type on which to create the field.
|
|
1964
|
+
data_type: Data type for the new field.
|
|
1965
|
+
display_name: Display name for the new field.
|
|
1966
|
+
properties: Optional additional field properties.
|
|
1967
|
+
|
|
1968
|
+
Returns:
|
|
1969
|
+
Internal name of the created field.
|
|
1970
|
+
|
|
1971
|
+
Raises:
|
|
1972
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
1973
|
+
or if the server result is invalid.
|
|
1974
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
1975
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
1976
|
+
policy.
|
|
1977
|
+
"""
|
|
1978
|
+
result = self._call(
|
|
1979
|
+
'schema_field_create',
|
|
1980
|
+
_methods.build_schema_field_create_params(
|
|
1981
|
+
entity_type,
|
|
1982
|
+
data_type,
|
|
1983
|
+
display_name,
|
|
1984
|
+
properties,
|
|
1985
|
+
),
|
|
1986
|
+
)
|
|
1987
|
+
return _methods.require_string_result(result, 'schema_field_create')
|
|
1988
|
+
|
|
1989
|
+
def schema_field_delete(self, entity_type: str, field_name: str) -> bool:
|
|
1990
|
+
"""Delete a field.
|
|
1991
|
+
|
|
1992
|
+
Args:
|
|
1993
|
+
entity_type: Entity type containing the field.
|
|
1994
|
+
field_name: Internal field name to delete.
|
|
1995
|
+
|
|
1996
|
+
Returns:
|
|
1997
|
+
Whether the server deleted the field.
|
|
1998
|
+
|
|
1999
|
+
Raises:
|
|
2000
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2001
|
+
or if the server result is invalid.
|
|
2002
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2003
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2004
|
+
policy.
|
|
2005
|
+
"""
|
|
2006
|
+
return _crud.require_boolean_result(
|
|
2007
|
+
self._call(
|
|
2008
|
+
'schema_field_delete',
|
|
2009
|
+
_methods.build_schema_field_delete_params(entity_type, field_name),
|
|
2010
|
+
),
|
|
2011
|
+
'schema_field_delete',
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
def schema_field_read(
|
|
2015
|
+
self,
|
|
2016
|
+
entity_type: str,
|
|
2017
|
+
field_name: str | None = None,
|
|
2018
|
+
project_entity: dict[str, Any] | None = None,
|
|
2019
|
+
) -> dict[str, object]:
|
|
2020
|
+
"""Return field schema, optionally for one field or project.
|
|
2021
|
+
|
|
2022
|
+
Args:
|
|
2023
|
+
entity_type: Entity type whose fields to return.
|
|
2024
|
+
field_name: Optional internal field name to return.
|
|
2025
|
+
project_entity: Optional project entity restricting the schema.
|
|
2026
|
+
|
|
2027
|
+
Returns:
|
|
2028
|
+
Field schema data returned by the server.
|
|
2029
|
+
|
|
2030
|
+
Raises:
|
|
2031
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2032
|
+
or if the server result is invalid.
|
|
2033
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2034
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2035
|
+
policy.
|
|
2036
|
+
"""
|
|
2037
|
+
return _crud.require_dictionary_result(
|
|
2038
|
+
self._call(
|
|
2039
|
+
'schema_field_read',
|
|
2040
|
+
_methods.build_schema_field_read_params(
|
|
2041
|
+
entity_type,
|
|
2042
|
+
field_name,
|
|
2043
|
+
project_entity,
|
|
2044
|
+
),
|
|
2045
|
+
_InvocationOptions(
|
|
2046
|
+
compatibility_warning_codes=_methods.consume_compatibility_warnings(
|
|
2047
|
+
_methods.SCHEMA_FIELD_READ_WARNINGS,
|
|
2048
|
+
self,
|
|
2049
|
+
),
|
|
2050
|
+
),
|
|
2051
|
+
),
|
|
2052
|
+
'schema_field_read',
|
|
2053
|
+
)
|
|
2054
|
+
|
|
2055
|
+
def schema(self, entity_type: str) -> dict[str, object]:
|
|
2056
|
+
"""Return field schema through the deprecated alias.
|
|
2057
|
+
|
|
2058
|
+
Args:
|
|
2059
|
+
entity_type: Entity type whose fields to return.
|
|
2060
|
+
|
|
2061
|
+
Returns:
|
|
2062
|
+
Field schema data returned by the server.
|
|
2063
|
+
|
|
2064
|
+
Raises:
|
|
2065
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2066
|
+
or if the server result is invalid.
|
|
2067
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2068
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2069
|
+
policy.
|
|
2070
|
+
"""
|
|
2071
|
+
_methods.warn_schema_alias()
|
|
2072
|
+
with _methods.schema_alias(self):
|
|
2073
|
+
return self.schema_field_read(entity_type)
|
|
2074
|
+
|
|
2075
|
+
def schema_field_update(
|
|
2076
|
+
self,
|
|
2077
|
+
entity_type: str,
|
|
2078
|
+
field_name: str,
|
|
2079
|
+
properties: dict[str, Any],
|
|
2080
|
+
project_entity: dict[str, Any] | None = None,
|
|
2081
|
+
) -> bool:
|
|
2082
|
+
"""Update field properties.
|
|
2083
|
+
|
|
2084
|
+
Args:
|
|
2085
|
+
entity_type: Entity type containing the field.
|
|
2086
|
+
field_name: Internal field name to update.
|
|
2087
|
+
properties: Field properties to update.
|
|
2088
|
+
project_entity: Optional project entity restricting the update.
|
|
2089
|
+
|
|
2090
|
+
Returns:
|
|
2091
|
+
Whether the server updated the field.
|
|
2092
|
+
|
|
2093
|
+
Raises:
|
|
2094
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2095
|
+
or if the server result is invalid.
|
|
2096
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2097
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2098
|
+
policy.
|
|
2099
|
+
"""
|
|
2100
|
+
return _crud.require_boolean_result(
|
|
2101
|
+
self._call(
|
|
2102
|
+
'schema_field_update',
|
|
2103
|
+
_methods.build_schema_field_update_params(
|
|
2104
|
+
entity_type,
|
|
2105
|
+
field_name,
|
|
2106
|
+
properties,
|
|
2107
|
+
project_entity,
|
|
2108
|
+
),
|
|
2109
|
+
),
|
|
2110
|
+
'schema_field_update',
|
|
2111
|
+
)
|
|
2112
|
+
|
|
2113
|
+
def schema_read(
|
|
2114
|
+
self,
|
|
2115
|
+
project_entity: dict[str, Any] | None = None,
|
|
2116
|
+
) -> dict[str, object]:
|
|
2117
|
+
"""Return the full schema, optionally for one project.
|
|
2118
|
+
|
|
2119
|
+
Args:
|
|
2120
|
+
project_entity: Optional project entity restricting the schema.
|
|
2121
|
+
|
|
2122
|
+
Returns:
|
|
2123
|
+
Full schema data returned by the server.
|
|
2124
|
+
|
|
2125
|
+
Raises:
|
|
2126
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2127
|
+
or if the server result is invalid.
|
|
2128
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2129
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2130
|
+
policy.
|
|
2131
|
+
"""
|
|
2132
|
+
params = _methods.build_schema_entity_read_params(project_entity)
|
|
2133
|
+
return _crud.require_dictionary_result(self._call('schema_read', params), 'schema_read')
|
|
2134
|
+
|
|
2135
|
+
@property
|
|
2136
|
+
def server_caps(self) -> ServerCapabilities:
|
|
2137
|
+
"""Return validated capabilities for the configured origin.
|
|
2138
|
+
|
|
2139
|
+
Returns:
|
|
2140
|
+
Server capability metadata at or above the supported floor.
|
|
2141
|
+
|
|
2142
|
+
Raises:
|
|
2143
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs.
|
|
2144
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2145
|
+
ValueError: If server parameters are missing or the configured
|
|
2146
|
+
origin lacks a valid host.
|
|
2147
|
+
"""
|
|
2148
|
+
context = config_module.snapshot(self._config)
|
|
2149
|
+
with self._driver_lease(context) as driver:
|
|
2150
|
+
return self._ensure_readiness(context, driver)
|
|
2151
|
+
|
|
2152
|
+
@property
|
|
2153
|
+
def server_info(self) -> dict[str, object]:
|
|
2154
|
+
"""Return raw validated server metadata.
|
|
2155
|
+
|
|
2156
|
+
Returns:
|
|
2157
|
+
The metadata that populated `server_caps`.
|
|
2158
|
+
|
|
2159
|
+
Raises:
|
|
2160
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs.
|
|
2161
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2162
|
+
ValueError: If server parameters are missing or the configured
|
|
2163
|
+
origin lacks a valid host.
|
|
2164
|
+
"""
|
|
2165
|
+
return self.server_caps.server_info
|
|
2166
|
+
|
|
2167
|
+
def set_session_uuid(self, session_uuid: str) -> None:
|
|
2168
|
+
"""Set the browser session UUID for subsequent requests.
|
|
2169
|
+
|
|
2170
|
+
Args:
|
|
2171
|
+
session_uuid: Browser session UUID to publish.
|
|
2172
|
+
"""
|
|
2173
|
+
self._config.set_session_uuid(session_uuid)
|
|
2174
|
+
|
|
2175
|
+
def share_thumbnail(
|
|
2176
|
+
self,
|
|
2177
|
+
entities: list[dict[str, Any]],
|
|
2178
|
+
thumbnail_path: str | None = None,
|
|
2179
|
+
source_entity: dict[str, Any] | None = None,
|
|
2180
|
+
filmstrip_thumbnail: bool = False,
|
|
2181
|
+
**kwargs: object,
|
|
2182
|
+
) -> int:
|
|
2183
|
+
"""Associate one thumbnail with one or more entities.
|
|
2184
|
+
|
|
2185
|
+
Supply exactly one of `thumbnail_path` and `source_entity`. A local
|
|
2186
|
+
thumbnail is uploaded to the first target before it is shared with the
|
|
2187
|
+
remaining targets.
|
|
2188
|
+
|
|
2189
|
+
Args:
|
|
2190
|
+
entities: Nonempty list of target entities with `type` and `id`.
|
|
2191
|
+
thumbnail_path: Optional local thumbnail path to upload once.
|
|
2192
|
+
source_entity: Optional entity whose existing thumbnail is shared.
|
|
2193
|
+
filmstrip_thumbnail: Whether to share a filmstrip thumbnail.
|
|
2194
|
+
**kwargs: Additional upload arguments for path-based sharing;
|
|
2195
|
+
`field_name` is not accepted. Ignored for an existing source.
|
|
2196
|
+
|
|
2197
|
+
Returns:
|
|
2198
|
+
Attachment ID representing the source thumbnail.
|
|
2199
|
+
|
|
2200
|
+
Raises:
|
|
2201
|
+
TypeError: If forwarded arguments do not match the upload signature
|
|
2202
|
+
or include `field_name`.
|
|
2203
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2204
|
+
or if the source selection, entities, or transport fails.
|
|
2205
|
+
ShotgunThumbnailNotReady: If an uploaded source stays transient.
|
|
2206
|
+
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2207
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2208
|
+
policy.
|
|
2209
|
+
"""
|
|
2210
|
+
share = _files.prepare_thumbnail_share(
|
|
2211
|
+
entities,
|
|
2212
|
+
thumbnail_path,
|
|
2213
|
+
source_entity,
|
|
2214
|
+
filmstrip_thumbnail,
|
|
2215
|
+
)
|
|
2216
|
+
if share.thumbnail_path is not None and 'field_name' in kwargs:
|
|
2217
|
+
raise TypeError("Shotgun.upload() got multiple values for argument 'field_name'")
|
|
2218
|
+
prepared: _files.PreparedUpload | None = None
|
|
2219
|
+
if share.thumbnail_path is not None:
|
|
2220
|
+
arguments = _bind_upload(
|
|
2221
|
+
(
|
|
2222
|
+
share.source_entity_type,
|
|
2223
|
+
share.source_entity_id,
|
|
2224
|
+
share.thumbnail_path,
|
|
2225
|
+
),
|
|
2226
|
+
{
|
|
2227
|
+
**kwargs,
|
|
2228
|
+
'field_name': (
|
|
2229
|
+
'filmstrip_thumb_image' if filmstrip_thumbnail else 'thumb_image'
|
|
2230
|
+
),
|
|
2231
|
+
},
|
|
2232
|
+
)
|
|
2233
|
+
prepared = _files.prepare_upload(
|
|
2234
|
+
_files.UploadOptions(
|
|
2235
|
+
arguments=arguments,
|
|
2236
|
+
upload_type='Thumbnail',
|
|
2237
|
+
filmstrip=filmstrip_thumbnail,
|
|
2238
|
+
)
|
|
2239
|
+
)
|
|
2240
|
+
context = config_module.snapshot(self._config)
|
|
2241
|
+
require_secure_rpc(context, include_auth=True)
|
|
2242
|
+
with self._driver_lease(context) as driver:
|
|
2243
|
+
if prepared is not None:
|
|
2244
|
+
attachment_id = self._upload_thumbnail_for(driver, context, prepared)
|
|
2245
|
+
if len(share.entities) == 1:
|
|
2246
|
+
return attachment_id
|
|
2247
|
+
field_name: Literal['filmstrip_image', 'image'] = (
|
|
2248
|
+
'filmstrip_image' if filmstrip_thumbnail else 'image'
|
|
2249
|
+
)
|
|
2250
|
+
ready = driver.poll(
|
|
2251
|
+
lambda: self._thumbnail_source_ready(
|
|
2252
|
+
driver,
|
|
2253
|
+
context,
|
|
2254
|
+
share.source_entity_type,
|
|
2255
|
+
share.source_entity_id,
|
|
2256
|
+
field_name,
|
|
2257
|
+
),
|
|
2258
|
+
attempts=25,
|
|
2259
|
+
interval=context.rpc_attempt_interval / 1000.0,
|
|
2260
|
+
)
|
|
2261
|
+
if not ready:
|
|
2262
|
+
raise _errors.ShotgunThumbnailNotReady('thumbnail source did not become ready')
|
|
2263
|
+
return self._share_thumbnail_source(
|
|
2264
|
+
driver,
|
|
2265
|
+
context,
|
|
2266
|
+
share.entities[1:],
|
|
2267
|
+
share.source_entity,
|
|
2268
|
+
filmstrip_thumbnail,
|
|
2269
|
+
)
|
|
2270
|
+
self._ensure_readiness(context, driver)
|
|
2271
|
+
return self._share_thumbnail_source(
|
|
2272
|
+
driver,
|
|
2273
|
+
context,
|
|
2274
|
+
share.entities,
|
|
2275
|
+
share.source_entity,
|
|
2276
|
+
filmstrip_thumbnail,
|
|
2277
|
+
)
|
|
2278
|
+
|
|
2279
|
+
@overload
|
|
2280
|
+
def summarize(self, entity_type: Never) -> dict[str, object]: ...
|
|
2281
|
+
|
|
2282
|
+
@overload
|
|
2283
|
+
def summarize(
|
|
2284
|
+
self,
|
|
2285
|
+
entity_type: str,
|
|
2286
|
+
filters: list[Any] | tuple[Any, ...] | dict[str, Any],
|
|
2287
|
+
summary_fields: list[dict[str, Any]],
|
|
2288
|
+
filter_operator: str | None = None,
|
|
2289
|
+
grouping: list[dict[str, Any]] | None = None,
|
|
2290
|
+
include_archived_projects: bool = True,
|
|
2291
|
+
) -> dict[str, object]: ...
|
|
2292
|
+
|
|
2293
|
+
@_methods.expose_summarize_signature
|
|
2294
|
+
def summarize(self, *args: object, **kwargs: object) -> dict[str, object]:
|
|
2295
|
+
"""Summarize field data returned by a query.
|
|
2296
|
+
|
|
2297
|
+
Args:
|
|
2298
|
+
*args: Positional values from the documented `summarize` signature.
|
|
2299
|
+
**kwargs: Keyword values from the documented `summarize` signature.
|
|
2300
|
+
|
|
2301
|
+
Returns:
|
|
2302
|
+
Dict with `summaries` and (when grouped) `groups`.
|
|
2303
|
+
|
|
2304
|
+
Raises:
|
|
2305
|
+
TypeError: If arguments do not match the documented signature.
|
|
2306
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2307
|
+
or if `filters`, `summary_fields`, or the server result has the
|
|
2308
|
+
wrong type.
|
|
2309
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2310
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2311
|
+
policy.
|
|
2312
|
+
"""
|
|
2313
|
+
options = _methods.bind_summarize(args, kwargs, 'Shotgun')
|
|
2314
|
+
params = _methods.build_summarize_params(options)
|
|
2315
|
+
return _crud.require_dictionary_result(self._call('summarize', params), 'summarize')
|
|
2316
|
+
|
|
2317
|
+
def text_search(
|
|
2318
|
+
self,
|
|
2319
|
+
text: str,
|
|
2320
|
+
entity_types: dict[str, Any],
|
|
2321
|
+
project_ids: list[int] | None = None,
|
|
2322
|
+
limit: int | None = None,
|
|
2323
|
+
) -> dict[str, object]:
|
|
2324
|
+
"""Search entity display names matching `text` (min 3 chars server-side).
|
|
2325
|
+
|
|
2326
|
+
Args:
|
|
2327
|
+
text: Text to match.
|
|
2328
|
+
entity_types: Mapping of entity type to filters restricting that
|
|
2329
|
+
type.
|
|
2330
|
+
project_ids: Optional project id allowlist.
|
|
2331
|
+
limit: Maximum results to return.
|
|
2332
|
+
|
|
2333
|
+
Returns:
|
|
2334
|
+
Dict describing matches, as returned by the server.
|
|
2335
|
+
|
|
2336
|
+
Raises:
|
|
2337
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2338
|
+
or if `entity_types` or the server result is not a dictionary.
|
|
2339
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2340
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2341
|
+
policy.
|
|
2342
|
+
"""
|
|
2343
|
+
params = _methods.build_text_search_params(
|
|
2344
|
+
text,
|
|
2345
|
+
entity_types,
|
|
2346
|
+
project_ids,
|
|
2347
|
+
limit,
|
|
2348
|
+
)
|
|
2349
|
+
return _crud.require_dictionary_result(
|
|
2350
|
+
self._call(
|
|
2351
|
+
'query_display_name_cache',
|
|
2352
|
+
params,
|
|
2353
|
+
invocation_options=_InvocationOptions(logical_method_name='text_search'),
|
|
2354
|
+
),
|
|
2355
|
+
'text_search',
|
|
2356
|
+
)
|
|
2357
|
+
|
|
2358
|
+
def unfollow(self, user: dict[str, Any], entity: dict[str, Any]) -> dict[str, object]:
|
|
2359
|
+
"""Make a user stop following an entity.
|
|
2360
|
+
|
|
2361
|
+
Args:
|
|
2362
|
+
user: User entity that will stop following the target.
|
|
2363
|
+
entity: Entity to stop following.
|
|
2364
|
+
|
|
2365
|
+
Returns:
|
|
2366
|
+
Unfollow result returned by the server.
|
|
2367
|
+
|
|
2368
|
+
Raises:
|
|
2369
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2370
|
+
or if an argument or the server result has the wrong type.
|
|
2371
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2372
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2373
|
+
policy.
|
|
2374
|
+
"""
|
|
2375
|
+
params = _methods.build_follow_params(user, entity)
|
|
2376
|
+
return _crud.require_dictionary_result(self._call('unfollow', params), 'unfollow')
|
|
2377
|
+
|
|
2378
|
+
def update(
|
|
2379
|
+
self,
|
|
2380
|
+
entity_type: str,
|
|
2381
|
+
entity_id: int,
|
|
2382
|
+
data: dict[str, Any],
|
|
2383
|
+
multi_entity_update_modes: dict[str, str] | None = None,
|
|
2384
|
+
) -> dict[str, object]:
|
|
2385
|
+
"""Update fields on an existing entity.
|
|
2386
|
+
|
|
2387
|
+
Args:
|
|
2388
|
+
entity_type: Entity type to update.
|
|
2389
|
+
entity_id: Numeric entity identifier.
|
|
2390
|
+
data: Replacement field values.
|
|
2391
|
+
multi_entity_update_modes: Per-field multi-entity update modes.
|
|
2392
|
+
|
|
2393
|
+
Returns:
|
|
2394
|
+
The updated entity data returned by the server.
|
|
2395
|
+
|
|
2396
|
+
Raises:
|
|
2397
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2398
|
+
or if `data`, its field names, `multi_entity_update_modes`, or
|
|
2399
|
+
the server result is invalid.
|
|
2400
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2401
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2402
|
+
policy.
|
|
2403
|
+
"""
|
|
2404
|
+
params = _crud.build_update_params(
|
|
2405
|
+
entity_type,
|
|
2406
|
+
entity_id,
|
|
2407
|
+
data,
|
|
2408
|
+
multi_entity_update_modes,
|
|
2409
|
+
)
|
|
2410
|
+
return _crud.require_dictionary_result(self._call('update', params), 'update')
|
|
2411
|
+
|
|
2412
|
+
def update_project_last_accessed(
|
|
2413
|
+
self,
|
|
2414
|
+
project: dict[str, Any],
|
|
2415
|
+
user: dict[str, Any] | None = None,
|
|
2416
|
+
) -> None:
|
|
2417
|
+
"""Record that a project was accessed by the current or given user.
|
|
2418
|
+
|
|
2419
|
+
Args:
|
|
2420
|
+
project: Project entity whose access time to update.
|
|
2421
|
+
user: Optional user entity to attribute the access to.
|
|
2422
|
+
|
|
2423
|
+
Raises:
|
|
2424
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs.
|
|
2425
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2426
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2427
|
+
policy.
|
|
2428
|
+
"""
|
|
2429
|
+
self._call(
|
|
2430
|
+
'update_project_last_accessed_by_current_user',
|
|
2431
|
+
_methods.build_update_project_last_accessed_params(project, user),
|
|
2432
|
+
invocation_options=_InvocationOptions(
|
|
2433
|
+
logical_method_name='update_project_last_accessed'
|
|
2434
|
+
),
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
@overload
|
|
2438
|
+
def upload(self, entity_type: Never) -> int: ...
|
|
2439
|
+
|
|
2440
|
+
@overload
|
|
2441
|
+
def upload(
|
|
2442
|
+
self,
|
|
2443
|
+
entity_type: str,
|
|
2444
|
+
entity_id: int,
|
|
2445
|
+
path: str | bytes,
|
|
2446
|
+
field_name: str | None = None,
|
|
2447
|
+
display_name: str | None = None,
|
|
2448
|
+
tag_list: str | None = None,
|
|
2449
|
+
created_at: _dt_module.datetime | None = None,
|
|
2450
|
+
) -> int: ...
|
|
2451
|
+
|
|
2452
|
+
@_expose_upload_signature
|
|
2453
|
+
def upload(self, *args: object, **kwargs: object) -> int:
|
|
2454
|
+
"""Upload a file or thumbnail and return its Attachment ID.
|
|
2455
|
+
|
|
2456
|
+
A successful HTTP response from the legacy form-upload route still
|
|
2457
|
+
raises `ShotgunError`; no Attachment ID is established for that route.
|
|
2458
|
+
The form request has already been sent when this error is raised.
|
|
2459
|
+
|
|
2460
|
+
Args:
|
|
2461
|
+
*args: Positional arguments from the documented upload signature;
|
|
2462
|
+
the path may be text or filesystem-encoded bytes.
|
|
2463
|
+
**kwargs: Keyword arguments from the documented upload signature.
|
|
2464
|
+
|
|
2465
|
+
Returns:
|
|
2466
|
+
Created Attachment ID.
|
|
2467
|
+
|
|
2468
|
+
Raises:
|
|
2469
|
+
TypeError: If arguments do not match the documented signature.
|
|
2470
|
+
ShotgunError: If the file or an upload response is invalid, an RPC
|
|
2471
|
+
fault or terminal transport failure occurs, or the legacy
|
|
2472
|
+
form-upload route succeeds.
|
|
2473
|
+
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2474
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2475
|
+
policy.
|
|
2476
|
+
"""
|
|
2477
|
+
arguments = _bind_upload(args, kwargs)
|
|
2478
|
+
thumbnail_fields = {
|
|
2479
|
+
'filmstrip_image',
|
|
2480
|
+
'filmstrip_thumb_image',
|
|
2481
|
+
'image',
|
|
2482
|
+
'thumb_image',
|
|
2483
|
+
}
|
|
2484
|
+
upload_type: Literal['Attachment', 'Thumbnail'] = (
|
|
2485
|
+
'Thumbnail' if arguments.field_name in thumbnail_fields else 'Attachment'
|
|
2486
|
+
)
|
|
2487
|
+
return self._upload(
|
|
2488
|
+
_files.UploadOptions(
|
|
2489
|
+
arguments=arguments,
|
|
2490
|
+
upload_type=upload_type,
|
|
2491
|
+
filmstrip=arguments.field_name in {'filmstrip_image', 'filmstrip_thumb_image'},
|
|
2492
|
+
)
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
def upload_filmstrip_thumbnail(
|
|
2496
|
+
self,
|
|
2497
|
+
entity_type: str,
|
|
2498
|
+
entity_id: int,
|
|
2499
|
+
path: str,
|
|
2500
|
+
**kwargs: Unpack[_files.ThumbnailUploadKeywordArguments],
|
|
2501
|
+
) -> int:
|
|
2502
|
+
"""Upload a filmstrip thumbnail through `upload`.
|
|
2503
|
+
|
|
2504
|
+
Args:
|
|
2505
|
+
entity_type: Entity type receiving the thumbnail.
|
|
2506
|
+
entity_id: Numeric identifier of the receiving entity.
|
|
2507
|
+
path: Existing non-empty image path.
|
|
2508
|
+
**kwargs: Additional arguments forwarded to `upload`.
|
|
2509
|
+
|
|
2510
|
+
Returns:
|
|
2511
|
+
Created Attachment ID.
|
|
2512
|
+
|
|
2513
|
+
Raises:
|
|
2514
|
+
TypeError: If forwarded arguments do not match `upload`.
|
|
2515
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2516
|
+
or if the file, upload response, or transport fails.
|
|
2517
|
+
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2518
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2519
|
+
policy.
|
|
2520
|
+
"""
|
|
2521
|
+
return self.upload(
|
|
2522
|
+
entity_type,
|
|
2523
|
+
entity_id,
|
|
2524
|
+
path,
|
|
2525
|
+
field_name='filmstrip_thumb_image',
|
|
2526
|
+
**kwargs,
|
|
2527
|
+
)
|
|
2528
|
+
|
|
2529
|
+
def upload_thumbnail(
|
|
2530
|
+
self,
|
|
2531
|
+
entity_type: str,
|
|
2532
|
+
entity_id: int,
|
|
2533
|
+
path: str,
|
|
2534
|
+
**kwargs: Unpack[_files.ThumbnailUploadKeywordArguments],
|
|
2535
|
+
) -> int:
|
|
2536
|
+
"""Upload a static thumbnail through `upload`.
|
|
2537
|
+
|
|
2538
|
+
Args:
|
|
2539
|
+
entity_type: Entity type receiving the thumbnail.
|
|
2540
|
+
entity_id: Numeric identifier of the receiving entity.
|
|
2541
|
+
path: Existing non-empty image path.
|
|
2542
|
+
**kwargs: Additional arguments forwarded to `upload`.
|
|
2543
|
+
|
|
2544
|
+
Returns:
|
|
2545
|
+
Created Attachment ID.
|
|
2546
|
+
|
|
2547
|
+
Raises:
|
|
2548
|
+
TypeError: If forwarded arguments do not match `upload`.
|
|
2549
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2550
|
+
or if the file, upload response, or transport fails.
|
|
2551
|
+
ProtocolError: If an upload endpoint returns an HTTP error.
|
|
2552
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2553
|
+
policy.
|
|
2554
|
+
"""
|
|
2555
|
+
return self.upload(
|
|
2556
|
+
entity_type,
|
|
2557
|
+
entity_id,
|
|
2558
|
+
path,
|
|
2559
|
+
field_name='thumb_image',
|
|
2560
|
+
**kwargs,
|
|
2561
|
+
)
|
|
2562
|
+
|
|
2563
|
+
def user_subscriptions_create(self, users: list[dict[str, Any]]) -> bool:
|
|
2564
|
+
"""Assign subscriptions to users.
|
|
2565
|
+
|
|
2566
|
+
Args:
|
|
2567
|
+
users: Subscription dictionaries that each contain a `user_id` key.
|
|
2568
|
+
|
|
2569
|
+
Returns:
|
|
2570
|
+
Whether the server created the subscriptions.
|
|
2571
|
+
|
|
2572
|
+
Raises:
|
|
2573
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2574
|
+
or if `users` is not a list of dictionaries that each contain a
|
|
2575
|
+
`user_id` key, or the server result is not a boolean.
|
|
2576
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2577
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2578
|
+
policy.
|
|
2579
|
+
"""
|
|
2580
|
+
return _crud.require_boolean_result(
|
|
2581
|
+
self._call(
|
|
2582
|
+
'user_subscriptions_create',
|
|
2583
|
+
_methods.build_user_subscriptions_create_params(users),
|
|
2584
|
+
),
|
|
2585
|
+
'user_subscriptions_create',
|
|
2586
|
+
)
|
|
2587
|
+
|
|
2588
|
+
def user_subscriptions_read(self) -> list[dict[str, object]]:
|
|
2589
|
+
"""List the current user's subscriptions.
|
|
2590
|
+
|
|
2591
|
+
Returns:
|
|
2592
|
+
User subscription dictionaries returned by the server.
|
|
2593
|
+
|
|
2594
|
+
Raises:
|
|
2595
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2596
|
+
or if the server result is not a list of dictionaries.
|
|
2597
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2598
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2599
|
+
policy.
|
|
2600
|
+
"""
|
|
2601
|
+
return _methods.require_dictionary_list_result(
|
|
2602
|
+
self._call('user_subscriptions_read', None),
|
|
2603
|
+
'user_subscriptions_read',
|
|
2604
|
+
)
|
|
2605
|
+
|
|
2606
|
+
def work_schedule_read(
|
|
2607
|
+
self,
|
|
2608
|
+
start_date: _dt_module.date | str,
|
|
2609
|
+
end_date: _dt_module.date | str,
|
|
2610
|
+
project: dict[str, Any] | None = None,
|
|
2611
|
+
user: dict[str, Any] | None = None,
|
|
2612
|
+
) -> dict[str, object]:
|
|
2613
|
+
"""Read working-day rules between two dates.
|
|
2614
|
+
|
|
2615
|
+
Args:
|
|
2616
|
+
start_date: Inclusive date or exact `YYYY-MM-DD` string.
|
|
2617
|
+
end_date: Inclusive date or exact `YYYY-MM-DD` string.
|
|
2618
|
+
project: Optional project entity restriction.
|
|
2619
|
+
user: Optional user entity restriction.
|
|
2620
|
+
|
|
2621
|
+
Returns:
|
|
2622
|
+
Work schedule data returned by the server.
|
|
2623
|
+
|
|
2624
|
+
Raises:
|
|
2625
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2626
|
+
or if dates, restrictions, or the server result are invalid.
|
|
2627
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2628
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2629
|
+
policy.
|
|
2630
|
+
"""
|
|
2631
|
+
params = _methods.build_work_schedule_read_params(
|
|
2632
|
+
start_date,
|
|
2633
|
+
end_date,
|
|
2634
|
+
project,
|
|
2635
|
+
user,
|
|
2636
|
+
)
|
|
2637
|
+
return _crud.require_dictionary_result(
|
|
2638
|
+
self._call('work_schedule_read', params),
|
|
2639
|
+
'work_schedule_read',
|
|
2640
|
+
)
|
|
2641
|
+
|
|
2642
|
+
@overload
|
|
2643
|
+
def work_schedule_update(self, date: Never) -> dict[str, object]: ...
|
|
2644
|
+
|
|
2645
|
+
@overload
|
|
2646
|
+
def work_schedule_update(
|
|
2647
|
+
self,
|
|
2648
|
+
date: _dt_module.date | str,
|
|
2649
|
+
working: bool,
|
|
2650
|
+
description: str | None = None,
|
|
2651
|
+
project: dict[str, Any] | None = None,
|
|
2652
|
+
user: dict[str, Any] | None = None,
|
|
2653
|
+
recalculate_field: str | None = None,
|
|
2654
|
+
) -> dict[str, object]: ...
|
|
2655
|
+
|
|
2656
|
+
@_methods.expose_work_schedule_update_signature
|
|
2657
|
+
def work_schedule_update(self, *args: object, **kwargs: object) -> dict[str, object]:
|
|
2658
|
+
"""Update a work schedule using the documented arguments.
|
|
2659
|
+
|
|
2660
|
+
Args:
|
|
2661
|
+
*args: Positional values from the documented `work_schedule_update`
|
|
2662
|
+
signature.
|
|
2663
|
+
**kwargs: Keyword values from the documented `work_schedule_update`
|
|
2664
|
+
signature.
|
|
2665
|
+
|
|
2666
|
+
Returns:
|
|
2667
|
+
Updated work schedule data returned by the server.
|
|
2668
|
+
|
|
2669
|
+
Raises:
|
|
2670
|
+
TypeError: If arguments do not match the documented signature.
|
|
2671
|
+
ShotgunError: If an RPC fault or terminal transport failure occurs,
|
|
2672
|
+
or if dates, exclusivity, or the server result is invalid.
|
|
2673
|
+
ProtocolError: If a server request returns an HTTP error.
|
|
2674
|
+
ValueError: If authenticated HTTP violates the configured origin
|
|
2675
|
+
policy.
|
|
2676
|
+
"""
|
|
2677
|
+
options = _methods.bind_work_schedule_update(args, kwargs, 'Shotgun')
|
|
2678
|
+
params = _methods.build_work_schedule_update_params(options)
|
|
2679
|
+
return _crud.require_dictionary_result(
|
|
2680
|
+
self._call('work_schedule_update', params),
|
|
2681
|
+
'work_schedule_update',
|
|
2682
|
+
)
|