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.
@@ -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