sg-python-api 0.1.2__py3-none-any.whl → 0.2.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.
Files changed (56) hide show
  1. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/METADATA +21 -24
  2. sg_python_api-0.2.0.dist-info/RECORD +58 -0
  3. shotgun_api3/__init__.py +4 -0
  4. shotgun_api3/_async_client.py +347 -281
  5. shotgun_api3/_async_driver.py +25 -55
  6. shotgun_api3/_async_httpx2.py +92 -59
  7. shotgun_api3/_async_transport.py +38 -3
  8. shotgun_api3/_capabilities.py +4 -3
  9. shotgun_api3/_client.py +296 -422
  10. shotgun_api3/_config.py +245 -120
  11. shotgun_api3/_connections.py +144 -0
  12. shotgun_api3/_crud.py +25 -250
  13. shotgun_api3/_driver.py +29 -54
  14. shotgun_api3/_execution.py +1 -3
  15. shotgun_api3/_files.py +193 -21
  16. shotgun_api3/_methods.py +59 -411
  17. shotgun_api3/_protocol.py +27 -53
  18. shotgun_api3/_transport.py +82 -407
  19. shotgun_api3/_version.py +7 -2
  20. shotgun_api3/lib/mockgun/__init__.py +20 -7
  21. shotgun_api3/lib/mockgun/_activity.py +510 -0
  22. shotgun_api3/lib/mockgun/_async_client.py +196 -0
  23. shotgun_api3/lib/mockgun/_dates.py +164 -0
  24. shotgun_api3/lib/mockgun/_errors.py +18 -0
  25. shotgun_api3/lib/mockgun/_events.py +1031 -0
  26. shotgun_api3/lib/mockgun/_follows.py +217 -0
  27. shotgun_api3/lib/mockgun/_handlers.py +558 -0
  28. shotgun_api3/lib/mockgun/_links.py +440 -0
  29. shotgun_api3/lib/mockgun/_new_client.py +92 -0
  30. shotgun_api3/lib/mockgun/_presets.py +492 -0
  31. shotgun_api3/lib/mockgun/_query.py +2163 -0
  32. shotgun_api3/lib/mockgun/_retirement.py +368 -0
  33. shotgun_api3/lib/mockgun/_schema.py +172 -0
  34. shotgun_api3/lib/mockgun/_schema_writes.py +425 -0
  35. shotgun_api3/lib/mockgun/_search.py +378 -0
  36. shotgun_api3/lib/mockgun/_server.py +169 -0
  37. shotgun_api3/lib/mockgun/_sessions.py +206 -0
  38. shotgun_api3/lib/mockgun/_site.py +150 -0
  39. shotgun_api3/lib/mockgun/_site_data.py +584 -0
  40. shotgun_api3/lib/mockgun/_storage.py +208 -0
  41. shotgun_api3/lib/mockgun/_store.py +1446 -0
  42. shotgun_api3/lib/mockgun/_summaries.py +964 -0
  43. shotgun_api3/lib/mockgun/_sync_client.py +190 -0
  44. shotgun_api3/lib/mockgun/_threads.py +226 -0
  45. shotgun_api3/lib/mockgun/_thumbnails.py +146 -0
  46. shotgun_api3/lib/mockgun/_transport.py +145 -0
  47. shotgun_api3/lib/mockgun/_uploads.py +417 -0
  48. shotgun_api3/lib/mockgun/_wire.py +120 -0
  49. shotgun_api3/lib/mockgun/mockgun.py +14 -0
  50. shotgun_api3/lib/sgtimezone.py +24 -1
  51. sg_python_api-0.1.2.dist-info/RECORD +0 -31
  52. shotgun_api3/_signatures.py +0 -53
  53. shotgun_api3/lib/mockgun/_async.py +0 -315
  54. shotgun_api3/lib/mockgun/_mockgun.py +0 -1666
  55. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/WHEEL +0 -0
  56. {sg_python_api-0.1.2.dist-info → sg_python_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,208 @@
1
+ """Retain issued upload allocations and bytes independently of record transactions."""
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from urllib.parse import urlsplit
7
+ from uuid import uuid4
8
+
9
+ from ._errors import MockgunError
10
+ from ._wire import format_wire_datetime
11
+
12
+ STORAGE_HOST = 'mockgun-storage.invalid'
13
+ _LETTERS = str.maketrans('0123456789abcdef', 'abcdefghijklmnop')
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Allocation:
18
+ """One issued allocation whose encoded response is the link-step credential.
19
+
20
+ Attributes:
21
+ upload_id: Site-local upload identity, independent of record IDs.
22
+ filename: Client-supplied basename.
23
+ upload_type: Attachment or thumbnail allocation kind.
24
+ multipart: Whether the allocation requires part completion.
25
+ key: Opaque storage path component.
26
+ encoded: Exact allocation answer sent back by the link step.
27
+ """
28
+
29
+ upload_id: str
30
+ filename: str
31
+ upload_type: str
32
+ multipart: bool
33
+ key: str
34
+ encoded: str
35
+
36
+
37
+ class Storage:
38
+ """An in-memory byte store whose uploads survive failed record transactions.
39
+
40
+ Only issued URLs accept writes. Attachment bindings identify completed
41
+ files; reading a URL ignores its query token.
42
+ """
43
+
44
+ def __init__(self) -> None:
45
+ """Create empty allocations, bytes, parts and Attachment bindings."""
46
+ self._allocations: dict[str, Allocation] = {}
47
+ self._encoded: dict[str, Allocation] = {}
48
+ self._blobs: dict[str, bytes] = {}
49
+ self._parts: dict[str, tuple[str, int]] = {}
50
+ self._etags: dict[str, str] = {}
51
+ self._etag_serial = 0
52
+ self._bindings: dict[int, str] = {}
53
+
54
+ def allocate(
55
+ self, filename: str, upload_type: str, *, multipart: bool, now: datetime
56
+ ) -> Allocation:
57
+ """Issue an upload URL and retain its exact encoded answer.
58
+
59
+ Args:
60
+ filename: Basename supplied by the client.
61
+ upload_type: Allocation kind sent by the client.
62
+ multipart: Whether parts must be completed before linking.
63
+ now: Aware request instant used for the opaque timestamp.
64
+
65
+ Returns:
66
+ The retained allocation, including its upload URL and exact encoded
67
+ link-step credential.
68
+ """
69
+ key = _letters()
70
+ upload_id = f'mockgun-upload-{len(self._allocations) + 1}'
71
+ fields = [
72
+ '1',
73
+ f'https://{STORAGE_HOST}/{key}',
74
+ format_wire_datetime(now),
75
+ upload_type,
76
+ upload_id,
77
+ ]
78
+ if multipart:
79
+ fields.append('mockgun-multipart')
80
+ allocation = Allocation(
81
+ upload_id, filename, upload_type, multipart, key, '\n'.join(fields)
82
+ )
83
+ self._allocations[upload_id] = allocation
84
+ self._encoded[allocation.encoded] = allocation
85
+ return allocation
86
+
87
+ def allocation(self, encoded: str) -> Allocation:
88
+ """Resolve the exact answer returned by allocation.
89
+
90
+ Raises:
91
+ MockgunError: If the link form names an unissued allocation.
92
+ """
93
+ allocation = self._encoded.get(encoded)
94
+ if allocation is None:
95
+ raise MockgunError(
96
+ 'mockgun POST /upload/api_link_file upload_link_info: allocation was not issued'
97
+ )
98
+ return allocation
99
+
100
+ def bind(self, attachment_id: int, key: str) -> None:
101
+ """Associate an Attachment identity with its uploaded storage key."""
102
+ self._bindings[attachment_id] = key
103
+
104
+ def bound_key(self, attachment_id: int) -> str | None:
105
+ """Return the Attachment's storage key, or `None` when unbound."""
106
+ return self._bindings.get(attachment_id)
107
+
108
+ def complete(self, upload_id: str, etags: Sequence[str]) -> int:
109
+ """Join contiguous uploaded parts after checking their ordered ETags.
110
+
111
+ Args:
112
+ upload_id: Identity issued by a multipart allocation.
113
+ etags: Quoted ETags in one-based part order.
114
+
115
+ Returns:
116
+ The number of completed parts.
117
+
118
+ Raises:
119
+ MockgunError: If the allocation, parts or ETags cannot be
120
+ validated.
121
+ """
122
+ allocation = self._allocations.get(upload_id)
123
+ parts = sorted(
124
+ (number, path)
125
+ for path, (identity, number) in self._parts.items()
126
+ if identity == upload_id
127
+ )
128
+ if (
129
+ allocation is None
130
+ or not allocation.multipart
131
+ or not parts
132
+ or [number for number, _ in parts] != list(range(1, len(parts) + 1))
133
+ or [self._etags.get(path) for _, path in parts] != list(etags)
134
+ or any(path not in self._blobs for _, path in parts)
135
+ ):
136
+ raise MockgunError(
137
+ f'mockgun POST /upload/api_complete_multipart_upload upload_id {upload_id}: '
138
+ 'parts or ETags are unsettled'
139
+ )
140
+ self._blobs['/' + allocation.key] = b''.join(self._blobs[path] for _, path in parts)
141
+ return len(parts)
142
+
143
+ def has_bytes(self, key: str) -> bool:
144
+ """Return whether a completed file exists under the allocation key."""
145
+ return '/' + key in self._blobs
146
+
147
+ def image_url(self, key: str) -> str:
148
+ """Return the key's URL with a fresh opaque query token."""
149
+ return f'https://{STORAGE_HOST}/{key}?{_letters()[:16]}'
150
+
151
+ def part_url(self, upload_id: str, part_number: int) -> str:
152
+ """Issue a storage URL for a positive, one-based multipart part.
153
+
154
+ Raises:
155
+ MockgunError: If the upload is not multipart or the part is
156
+ invalid.
157
+ """
158
+ allocation = self._allocations.get(upload_id)
159
+ if allocation is None or not allocation.multipart or part_number < 1:
160
+ raise MockgunError(
161
+ f'mockgun POST /upload/api_get_upload_link_for_part upload_id {upload_id}: '
162
+ f'part_number {part_number} was not issued'
163
+ )
164
+ for path, identity in self._parts.items():
165
+ if identity == (upload_id, part_number):
166
+ return f'https://{STORAGE_HOST}{path}'
167
+ path = f'/{allocation.key}/part-{_letters()}'
168
+ self._parts[path] = (upload_id, part_number)
169
+ return f'https://{STORAGE_HOST}{path}'
170
+
171
+ def put(self, url: str, data: bytes) -> str | None:
172
+ """Store bytes at an issued URL, returning a quoted ETag for a part.
173
+
174
+ Raises:
175
+ MockgunError: If the URL was not issued for storage writes.
176
+ """
177
+ parsed = urlsplit(url)
178
+ path = parsed.path
179
+ direct = any(
180
+ '/' + item.key == path and not item.multipart for item in self._allocations.values()
181
+ )
182
+ if parsed.hostname != STORAGE_HOST or (path not in self._parts and not direct):
183
+ raise MockgunError(f'mockgun does not emulate PUT {path}: storage URL was not issued')
184
+ self._blobs[path] = data
185
+ if path not in self._parts:
186
+ return None
187
+ self._etag_serial += 1
188
+ etag = f'"mockgun-etag-{self._etag_serial}"'
189
+ self._etags[path] = etag
190
+ return etag
191
+
192
+ def read(self, url: str) -> bytes:
193
+ """Return uploaded bytes, ignoring a storage URL's query token.
194
+
195
+ Raises:
196
+ MockgunError: If the URL has no stored bytes.
197
+ """
198
+ parsed = urlsplit(url)
199
+ if parsed.hostname != STORAGE_HOST or parsed.path not in self._blobs:
200
+ raise MockgunError(
201
+ f'mockgun does not emulate GET {parsed.path}: storage bytes are unknown'
202
+ )
203
+ return self._blobs[parsed.path]
204
+
205
+
206
+ def _letters() -> str:
207
+ """Return an opaque path component containing no decimal digits."""
208
+ return uuid4().hex.translate(_LETTERS)