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/_errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Exception hierarchy for the `shotgun_api3` compatibility surface."""
|
|
2
|
+
|
|
3
|
+
from xmlrpc.client import Error, ProtocolError, ResponseError
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'AuthenticationFault',
|
|
7
|
+
'Error',
|
|
8
|
+
'Fault',
|
|
9
|
+
'MissingTwoFactorAuthenticationFault',
|
|
10
|
+
'ProtocolError',
|
|
11
|
+
'ResponseError',
|
|
12
|
+
'ShotgunError',
|
|
13
|
+
'ShotgunFileDownloadError',
|
|
14
|
+
'ShotgunThumbnailNotReady',
|
|
15
|
+
'UserCredentialsNotAllowedForOxygenAuthenticationFault',
|
|
16
|
+
'UserCredentialsNotAllowedForSSOAuthenticationFault',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ShotgunError(Exception):
|
|
21
|
+
"""Base exception for errors in the `ShotgunError` family."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Fault(ShotgunError): # noqa: N818
|
|
25
|
+
"""Raised when the server reports an exception in an RPC response."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthenticationFault(Fault):
|
|
29
|
+
"""Raised when the server rejects the provided credentials."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MissingTwoFactorAuthenticationFault(Fault):
|
|
33
|
+
"""Raised when two-factor authentication is required but missing."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ShotgunFileDownloadError(ShotgunError):
|
|
37
|
+
"""Raised when downloading an attachment fails."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ShotgunThumbnailNotReady(ShotgunError): # noqa: N818
|
|
41
|
+
"""Raised when a requested thumbnail has not finished processing."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UserCredentialsNotAllowedForOxygenAuthenticationFault(Fault):
|
|
45
|
+
"""Raised when username/password auth is used against an Autodesk Identity site."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserCredentialsNotAllowedForSSOAuthenticationFault(Fault):
|
|
49
|
+
"""Raised when username/password auth is used against an SSO-enabled site."""
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Share pure execution, attempt, and lease logic between the sync and async twins."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import Generic, Literal, TypeVar
|
|
7
|
+
|
|
8
|
+
from . import _protocol
|
|
9
|
+
from ._config import (
|
|
10
|
+
EventRoute,
|
|
11
|
+
Outcome,
|
|
12
|
+
RequestContext,
|
|
13
|
+
ResponseEvent,
|
|
14
|
+
RetryEvent,
|
|
15
|
+
notify_observer,
|
|
16
|
+
thaw_mapping,
|
|
17
|
+
)
|
|
18
|
+
from ._transport import TransportError, TransportRequest
|
|
19
|
+
|
|
20
|
+
_ControlT_co = TypeVar('_ControlT_co', covariant=True)
|
|
21
|
+
_T = TypeVar('_T')
|
|
22
|
+
_TransportT = TypeVar('_TransportT')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass(frozen=True)
|
|
26
|
+
class AttemptRetry:
|
|
27
|
+
"""Carry one post-cleanup retry event back to the executor."""
|
|
28
|
+
|
|
29
|
+
event: RetryEvent
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass(frozen=True)
|
|
33
|
+
class AttemptState(Generic[_ControlT_co]):
|
|
34
|
+
"""Bundle immutable metadata shared by one attempt's helper calls.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
context: Configuration captured for the logical operation.
|
|
38
|
+
control: Attempt-local deadline and progress control.
|
|
39
|
+
number: One-based attempt number.
|
|
40
|
+
operation: Logical operation and replay classification.
|
|
41
|
+
route: Observer route for the attempt.
|
|
42
|
+
started: Attempt-start time in seconds from the same monotonic clock
|
|
43
|
+
used by `AttemptOutcomes` to calculate response latency.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
context: RequestContext
|
|
47
|
+
control: _ControlT_co
|
|
48
|
+
number: int
|
|
49
|
+
operation: _protocol.OperationDescriptor
|
|
50
|
+
route: EventRoute
|
|
51
|
+
started: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass(frozen=True)
|
|
55
|
+
class AttemptOutcomes:
|
|
56
|
+
"""Choose retry outcomes and publish response events for one driver.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
clock: Monotonic clock returning seconds on the same time base as
|
|
60
|
+
`AttemptState.started`.
|
|
61
|
+
jitter_seed: Source of per-attempt high-resolution numeric seeds.
|
|
62
|
+
Values should differ between attempts; identical complete retry
|
|
63
|
+
inputs intentionally reproduce the same jitter.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
clock: Callable[[], float]
|
|
67
|
+
jitter_seed: Callable[[], float]
|
|
68
|
+
|
|
69
|
+
def emit_response(
|
|
70
|
+
self,
|
|
71
|
+
state: AttemptState[object],
|
|
72
|
+
status: int | None,
|
|
73
|
+
outcome: Outcome,
|
|
74
|
+
correlation_id: str | None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Publish one redacted response event outside driver locks."""
|
|
77
|
+
latency = max(0.0, self.clock() - state.started)
|
|
78
|
+
notify_observer(
|
|
79
|
+
state.context.on_response,
|
|
80
|
+
ResponseEvent(
|
|
81
|
+
operation=state.operation.category,
|
|
82
|
+
route=state.route,
|
|
83
|
+
attempt=state.number,
|
|
84
|
+
latency=latency,
|
|
85
|
+
status=status,
|
|
86
|
+
outcome=outcome,
|
|
87
|
+
correlation_id=correlation_id,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def retry(
|
|
92
|
+
self,
|
|
93
|
+
state: AttemptState[object],
|
|
94
|
+
failure: _protocol.Failure,
|
|
95
|
+
status: int | None,
|
|
96
|
+
) -> AttemptRetry | None:
|
|
97
|
+
"""Return the retry for one failed attempt, or `None` when retrying stops."""
|
|
98
|
+
delay = _protocol.decide(
|
|
99
|
+
state.operation,
|
|
100
|
+
state.number,
|
|
101
|
+
failure,
|
|
102
|
+
max_attempts=state.context.max_rpc_attempts,
|
|
103
|
+
base_interval=state.context.rpc_attempt_interval / 1000.0,
|
|
104
|
+
now=self.jitter_seed(),
|
|
105
|
+
)
|
|
106
|
+
if delay is None:
|
|
107
|
+
return None
|
|
108
|
+
return AttemptRetry(
|
|
109
|
+
RetryEvent(
|
|
110
|
+
operation=state.operation.category,
|
|
111
|
+
route=state.route,
|
|
112
|
+
attempt=state.number,
|
|
113
|
+
delay=delay,
|
|
114
|
+
status=status,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def send_failure(
|
|
119
|
+
self,
|
|
120
|
+
error: TransportError,
|
|
121
|
+
state: AttemptState[object],
|
|
122
|
+
) -> AttemptRetry | None:
|
|
123
|
+
"""Classify one pre-response transport failure and publish its event."""
|
|
124
|
+
retry = self.retry(
|
|
125
|
+
state,
|
|
126
|
+
_protocol.classify_transport_failure(error, response_received=False),
|
|
127
|
+
None,
|
|
128
|
+
)
|
|
129
|
+
self.emit_response(
|
|
130
|
+
state,
|
|
131
|
+
status=None,
|
|
132
|
+
outcome='error' if retry is None else 'retry',
|
|
133
|
+
correlation_id=None,
|
|
134
|
+
)
|
|
135
|
+
return retry
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclasses.dataclass(frozen=True)
|
|
139
|
+
class AttemptSuccess(Generic[_T]):
|
|
140
|
+
"""Carry one successful consumer result back to the executor."""
|
|
141
|
+
|
|
142
|
+
result: _T
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ExceptionCapture:
|
|
146
|
+
"""Record an operation-local exception without suppressing propagation.
|
|
147
|
+
|
|
148
|
+
Attributes:
|
|
149
|
+
exception: Exception from the most recent context exit, or `None`
|
|
150
|
+
before the first exit or after an exit without an exception.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
"""Initialize an empty exception capture."""
|
|
155
|
+
self.exception: BaseException | None = None
|
|
156
|
+
|
|
157
|
+
def __enter__(self) -> None:
|
|
158
|
+
"""Enter the exception boundary."""
|
|
159
|
+
|
|
160
|
+
def __exit__(
|
|
161
|
+
self,
|
|
162
|
+
_exception_type: type[BaseException] | None,
|
|
163
|
+
exception: BaseException | None,
|
|
164
|
+
_traceback: object,
|
|
165
|
+
) -> Literal[False]:
|
|
166
|
+
"""Record the current exception and preserve its propagation."""
|
|
167
|
+
self.exception = exception
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SuppressedExceptionCapture:
|
|
172
|
+
"""Record and suppress an exception for explicit precedence selection.
|
|
173
|
+
|
|
174
|
+
Attributes:
|
|
175
|
+
exception: Captured exception, or `None` when no exception was
|
|
176
|
+
captured. Reset to `None` on each context entry.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self) -> None:
|
|
180
|
+
"""Initialize an empty exception capture."""
|
|
181
|
+
self.exception: BaseException | None = None
|
|
182
|
+
|
|
183
|
+
def __enter__(self) -> None:
|
|
184
|
+
"""Reset the exception at entry to this capture boundary."""
|
|
185
|
+
self.exception = None
|
|
186
|
+
|
|
187
|
+
def __exit__(
|
|
188
|
+
self,
|
|
189
|
+
_exception_type: type[BaseException] | None,
|
|
190
|
+
exception: BaseException | None,
|
|
191
|
+
_traceback: object,
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""Record and suppress the current exception when present."""
|
|
194
|
+
self.exception = exception
|
|
195
|
+
return exception is not None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_rpc_request(
|
|
199
|
+
method_name: str,
|
|
200
|
+
params: object,
|
|
201
|
+
context: RequestContext,
|
|
202
|
+
*,
|
|
203
|
+
include_auth: bool,
|
|
204
|
+
) -> TransportRequest:
|
|
205
|
+
"""Build fresh RPC bytes, headers, auth, and token for one attempt.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
method_name: RPC method being invoked.
|
|
209
|
+
params: Invocation-owned parameters, already detached at call
|
|
210
|
+
boundaries.
|
|
211
|
+
context: Captured configuration and credentials for the invocation.
|
|
212
|
+
include_auth: Whether to include captured authentication fields.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A fresh request without copying the invocation-owned parameters again.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If server parameters have not been configured.
|
|
219
|
+
"""
|
|
220
|
+
api_url = context.api_url
|
|
221
|
+
if api_url is None:
|
|
222
|
+
raise ValueError('server parameters must be configured before RPC I/O')
|
|
223
|
+
auth = thaw_mapping(context.auth) if include_auth else None
|
|
224
|
+
payload = _protocol.build_payload(method_name, params, auth)
|
|
225
|
+
headers = {
|
|
226
|
+
'connection': 'keep-alive',
|
|
227
|
+
'content-type': 'application/json; charset=utf-8',
|
|
228
|
+
'user-agent': context.user_agent,
|
|
229
|
+
}
|
|
230
|
+
if context.localized:
|
|
231
|
+
headers['locale'] = 'auto'
|
|
232
|
+
return TransportRequest(
|
|
233
|
+
method='POST',
|
|
234
|
+
url=api_url,
|
|
235
|
+
headers=headers,
|
|
236
|
+
body=_protocol.encode_payload(payload),
|
|
237
|
+
timeout=context.timeout,
|
|
238
|
+
token=uuid.uuid4().hex,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def release_lease(
|
|
243
|
+
lease_counts: dict[int, int],
|
|
244
|
+
retired: dict[int, _TransportT],
|
|
245
|
+
transport: _TransportT,
|
|
246
|
+
) -> _TransportT | None:
|
|
247
|
+
"""Drop one lease and return a retired transport that this release left idle.
|
|
248
|
+
|
|
249
|
+
Call with the owning client's transport lock held.
|
|
250
|
+
"""
|
|
251
|
+
key = id(transport)
|
|
252
|
+
remaining = lease_counts[key] - 1
|
|
253
|
+
if remaining:
|
|
254
|
+
lease_counts[key] = remaining
|
|
255
|
+
return None
|
|
256
|
+
del lease_counts[key]
|
|
257
|
+
return retired.pop(key, None)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def retire_transport(
|
|
261
|
+
lease_counts: Mapping[int, int],
|
|
262
|
+
retired: dict[int, _TransportT],
|
|
263
|
+
transport: _TransportT,
|
|
264
|
+
) -> _TransportT | None:
|
|
265
|
+
"""Retire a transport and return it when no lease delays closing it.
|
|
266
|
+
|
|
267
|
+
Call with the owning client's transport lock held.
|
|
268
|
+
"""
|
|
269
|
+
key = id(transport)
|
|
270
|
+
if lease_counts.get(key, 0):
|
|
271
|
+
retired[key] = transport
|
|
272
|
+
return None
|
|
273
|
+
return transport
|