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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Async transport vocabulary and streaming response handling."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from ._transport import (
|
|
9
|
+
CaseInsensitiveHeaders,
|
|
10
|
+
TransportError,
|
|
11
|
+
TransportRequest,
|
|
12
|
+
TransportResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncStreamingResponse:
|
|
17
|
+
"""A response whose body is consumed incrementally exactly once.
|
|
18
|
+
|
|
19
|
+
Fully consuming the body releases response resources. Call `await close()`
|
|
20
|
+
when leaving the body unconsumed or stopping early. Explicit closure is
|
|
21
|
+
idempotent.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
status: HTTP status code.
|
|
25
|
+
reason: HTTP reason phrase.
|
|
26
|
+
headers: Response headers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
status: int,
|
|
32
|
+
reason: str,
|
|
33
|
+
headers: CaseInsensitiveHeaders,
|
|
34
|
+
stream: Callable[[int], AsyncIterator[bytes]],
|
|
35
|
+
close_fn: Callable[[], Awaitable[None]],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize a one-use asynchronous streaming response.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
status: HTTP status code.
|
|
41
|
+
reason: HTTP reason phrase.
|
|
42
|
+
headers: Response headers.
|
|
43
|
+
stream: Factory accepting a chunk size and returning the body
|
|
44
|
+
iterator.
|
|
45
|
+
close_fn: Awaitable callback that releases response resources.
|
|
46
|
+
"""
|
|
47
|
+
self.status = status
|
|
48
|
+
self.reason = reason
|
|
49
|
+
self.headers = headers
|
|
50
|
+
self._stream = stream
|
|
51
|
+
self._close_fn: Callable[[], Awaitable[None]] | None = close_fn
|
|
52
|
+
self._claimed = False
|
|
53
|
+
|
|
54
|
+
def _require_open(self) -> None:
|
|
55
|
+
"""Raise when response resources were already released."""
|
|
56
|
+
if self._close_fn is None:
|
|
57
|
+
raise TransportError('streaming response is closed')
|
|
58
|
+
|
|
59
|
+
async def close(self) -> None:
|
|
60
|
+
"""Release resources associated with the streaming response."""
|
|
61
|
+
close_fn = self._close_fn
|
|
62
|
+
self._close_fn = None
|
|
63
|
+
if close_fn is not None:
|
|
64
|
+
await close_fn()
|
|
65
|
+
|
|
66
|
+
async def _iterate(self, size: int) -> AsyncIterator[bytes]:
|
|
67
|
+
"""Yield chunks while closing on every terminal iterator path."""
|
|
68
|
+
try:
|
|
69
|
+
async for chunk in self._stream(size):
|
|
70
|
+
self._require_open()
|
|
71
|
+
yield chunk
|
|
72
|
+
except BaseException:
|
|
73
|
+
with suppress(BaseException):
|
|
74
|
+
await self.close()
|
|
75
|
+
raise
|
|
76
|
+
else:
|
|
77
|
+
await self.close()
|
|
78
|
+
|
|
79
|
+
def aiter_chunks(self, size: int = 65536) -> AsyncIterator[bytes]:
|
|
80
|
+
"""Claim and return the single-use response-body iterator.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
size: Maximum bytes per yielded chunk.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
An async iterator over the response body.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
TransportError: If `size` is not positive, the response is closed,
|
|
90
|
+
or its stream has already been claimed.
|
|
91
|
+
"""
|
|
92
|
+
if size <= 0:
|
|
93
|
+
raise TransportError('streaming chunk size must be positive')
|
|
94
|
+
self._require_open()
|
|
95
|
+
if self._claimed:
|
|
96
|
+
raise TransportError('streaming response was already consumed')
|
|
97
|
+
self._claimed = True
|
|
98
|
+
return self._iterate(size)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AsyncTransport(Protocol):
|
|
102
|
+
"""HTTP backend operations required by the async driver.
|
|
103
|
+
|
|
104
|
+
Task cancellation replaces the sync transport's `abort` operation. The
|
|
105
|
+
synchronous lease pair brackets one complete logical RPC so draining close
|
|
106
|
+
operations cannot interrupt a retry loop between attempts.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def checkin(self) -> None:
|
|
110
|
+
"""Release one logical RPC lease."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
def checkout(self) -> None:
|
|
114
|
+
"""Acquire one logical RPC lease."""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
async def close(self) -> None:
|
|
118
|
+
"""Release resources owned by the transport."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
def reserve(self, task: asyncio.Task[object]) -> None:
|
|
122
|
+
"""Retain one child task until its done callback runs.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
task: Child task whose scheduled work must survive close.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
TransportError: If close rejects the reserving owner or `task` is
|
|
129
|
+
already reserved.
|
|
130
|
+
"""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
async def send(self, request: TransportRequest) -> TransportResponse:
|
|
134
|
+
"""Send one request and return its complete response.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
request: Transport request to send.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The complete transport response.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
TransportError: If the transport operation fails.
|
|
144
|
+
"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
async def send_streaming(
|
|
148
|
+
self,
|
|
149
|
+
request: TransportRequest,
|
|
150
|
+
) -> AsyncStreamingResponse:
|
|
151
|
+
"""Send one request and return its single-use streaming response.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
request: Transport request to send.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The streaming transport response.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
TransportError: If the transport operation fails.
|
|
161
|
+
"""
|
|
162
|
+
...
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Server and client capability containers."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import ssl
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Literal, TypeAlias, cast
|
|
7
|
+
|
|
8
|
+
from ._errors import ShotgunError
|
|
9
|
+
|
|
10
|
+
_Platform: TypeAlias = Literal['linux', 'mac', 'windows']
|
|
11
|
+
|
|
12
|
+
_DEV_VERSION_INDEX = 3
|
|
13
|
+
_SERVER_VERSION_FLOOR = (7, 10, 0)
|
|
14
|
+
_VERSION_PARTS = 3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClientCapabilities:
|
|
18
|
+
"""Represent local platform details used by compatibility helpers.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
platform: Operating-system name.
|
|
22
|
+
py_version: Python version.
|
|
23
|
+
ssl_version: SSL library version.
|
|
24
|
+
local_path_field: Platform-specific local-file field name.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""Capture local platform capabilities for compatibility checks."""
|
|
29
|
+
system = sys.platform
|
|
30
|
+
if system == 'darwin':
|
|
31
|
+
platform: _Platform | None = 'mac'
|
|
32
|
+
elif system.startswith('linux'):
|
|
33
|
+
platform = 'linux'
|
|
34
|
+
elif system == 'win32':
|
|
35
|
+
platform = 'windows'
|
|
36
|
+
else:
|
|
37
|
+
platform = None
|
|
38
|
+
self.platform = platform
|
|
39
|
+
self.local_path_field = f'local_path_{platform}' if platform is not None else None
|
|
40
|
+
self.py_version = f'{sys.version_info.major}.{sys.version_info.minor}'
|
|
41
|
+
self.ssl_version = ssl.OPENSSL_VERSION
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServerCapabilities:
|
|
45
|
+
"""Represent parsed server version and capability metadata.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
host: Server host name.
|
|
49
|
+
version: Parsed server version.
|
|
50
|
+
is_dev: Whether the server reports a development build.
|
|
51
|
+
server_info: Detached raw server metadata.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, host: str, meta: dict[str, object]) -> None:
|
|
55
|
+
"""Parse one server's capability metadata.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
host: Server host name represented by the metadata.
|
|
59
|
+
meta: Server metadata containing a version sequence.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ShotgunError: If server version metadata is missing, short,
|
|
63
|
+
malformed, or below the supported floor.
|
|
64
|
+
"""
|
|
65
|
+
owned_meta = copy.deepcopy(meta)
|
|
66
|
+
self.host = host
|
|
67
|
+
self._server_info = owned_meta
|
|
68
|
+
raw = owned_meta.get('version')
|
|
69
|
+
if isinstance(raw, list | tuple):
|
|
70
|
+
sequence = cast('list[object] | tuple[object, ...]', raw)
|
|
71
|
+
self.is_dev = (
|
|
72
|
+
len(sequence) > _DEV_VERSION_INDEX and sequence[_DEV_VERSION_INDEX] == 'Dev'
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
self.is_dev = False
|
|
76
|
+
self.version = _version(owned_meta)
|
|
77
|
+
if self.version < _SERVER_VERSION_FLOOR:
|
|
78
|
+
floor = '.'.join(str(part) for part in _SERVER_VERSION_FLOOR)
|
|
79
|
+
current = '.'.join(str(part) for part in self.version)
|
|
80
|
+
raise ShotgunError(
|
|
81
|
+
f'This client requires server version {floor} or higher; server is {current}.'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
"""Return a concise capability summary."""
|
|
86
|
+
return (
|
|
87
|
+
f'ServerCapabilities: host {self.host}, version {self.version}, is_dev {self.is_dev}'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def ensure_include_archived_projects(self) -> bool:
|
|
91
|
+
"""Report archived-project filtering support.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Always `True`; the server floor guarantees this feature.
|
|
95
|
+
"""
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def ensure_paging_info_without_counts_support(self) -> bool:
|
|
99
|
+
"""Report count-free paging-info support.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Always `True`; the server floor guarantees this feature.
|
|
103
|
+
"""
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def ensure_per_project_customization(self) -> bool:
|
|
107
|
+
"""Report per-project customization support.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Always `True`; the server floor guarantees this feature.
|
|
111
|
+
"""
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def ensure_return_image_urls_support(self) -> bool:
|
|
115
|
+
"""Report image-URL return support.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Always `True`; the server floor guarantees this feature.
|
|
119
|
+
"""
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def ensure_support_for_additional_filter_presets(self) -> bool:
|
|
123
|
+
"""Report additional-filter-preset support.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Always `True`; the server floor guarantees this feature.
|
|
127
|
+
"""
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def ensure_user_following_support(self) -> bool:
|
|
131
|
+
"""Report user-following support.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Always `True`; the server floor guarantees this feature.
|
|
135
|
+
"""
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def server_info(self) -> dict[str, object]:
|
|
140
|
+
"""Return detached raw server metadata.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
A deep copy of the metadata owned by this capability container.
|
|
144
|
+
"""
|
|
145
|
+
return copy.deepcopy(self._server_info)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _version(meta: dict[str, object]) -> tuple[int, int, int]:
|
|
149
|
+
"""Parse the supported three-part server version."""
|
|
150
|
+
raw = meta.get('version')
|
|
151
|
+
if not isinstance(raw, list | tuple):
|
|
152
|
+
raise ShotgunError(
|
|
153
|
+
'The Flow Production Tracking server did not report a version; '
|
|
154
|
+
'it may be too old for this client.'
|
|
155
|
+
)
|
|
156
|
+
sequence = cast('list[object] | tuple[object, ...]', raw)
|
|
157
|
+
if len(sequence) < _VERSION_PARTS:
|
|
158
|
+
raise ShotgunError(
|
|
159
|
+
'The Flow Production Tracking server did not report a version; '
|
|
160
|
+
'it may be too old for this client.'
|
|
161
|
+
)
|
|
162
|
+
parts = sequence[:_VERSION_PARTS]
|
|
163
|
+
if any(not isinstance(part, int) or isinstance(part, bool) for part in parts):
|
|
164
|
+
raise ShotgunError('The Flow Production Tracking server reported an invalid version.')
|
|
165
|
+
return cast('tuple[int, int, int]', tuple(parts))
|