encore-dev 2.0.0a7__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.
- encore/__init__.py +43 -0
- encore/cron/__init__.py +50 -0
- encore/internal/__init__.py +1 -0
- encore/internal/_loader.py +79 -0
- encore/internal/_native.pyi +734 -0
- encore/internal/lazy.py +35 -0
- encore/internal/runtime.py +133 -0
- encore/internal/startup.py +24 -0
- encore/logs/__init__.py +19 -0
- encore/logs/_console.py +155 -0
- encore/logs/_runtime.py +56 -0
- encore/meta/__init__.py +228 -0
- encore/metrics/__init__.py +390 -0
- encore/pubsub/__init__.py +425 -0
- encore/py.typed +0 -0
- encore/secrets/__init__.py +139 -0
- encore/shutdown/__init__.py +164 -0
- encore/storage/__init__.py +5 -0
- encore/storage/objects/__init__.py +396 -0
- encore/storage/postgres/__init__.py +737 -0
- encore/storage/redis/__init__.py +200 -0
- encore/storage/redis/_base.py +42 -0
- encore/storage/redis/_responses.py +150 -0
- encore/storage/redis/_values.py +189 -0
- encore/storage/redis/bitmap.py +142 -0
- encore/storage/redis/generic.py +367 -0
- encore/storage/redis/geo.py +508 -0
- encore/storage/redis/hash.py +504 -0
- encore/storage/redis/json.py +614 -0
- encore/storage/redis/list.py +511 -0
- encore/storage/redis/pubsub.py +276 -0
- encore/storage/redis/set.py +570 -0
- encore/storage/redis/sorted_set.py +753 -0
- encore/storage/redis/stream.py +773 -0
- encore/storage/redis/string.py +654 -0
- encore/tracing/__init__.py +19 -0
- encore/tracing/_context.py +27 -0
- encore/tracing/_encode.py +351 -0
- encore/tracing/_provider.py +62 -0
- encore/tracing/_span.py +226 -0
- encore/tracing/_tracer.py +93 -0
- encore_dev-2.0.0a7.dist-info/METADATA +791 -0
- encore_dev-2.0.0a7.dist-info/RECORD +44 -0
- encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Start Encore shutdown and observe its progress."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from ..internal.runtime import EncoreError, runtime, _native
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ShutdownError(EncoreError):
|
|
9
|
+
"""Encore cleanup failed or shutdown progress could not be observed."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShutdownPhase(Enum):
|
|
13
|
+
"""A phase of the Encore runtime's shutdown process.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
NOT_STARTED: Shutdown has not started.
|
|
17
|
+
STARTED: Pub/Sub intake is stopping and active work is draining.
|
|
18
|
+
The host remains responsible for its HTTP server.
|
|
19
|
+
CLEANUP: Encore is cleaning up resources and flushing telemetry.
|
|
20
|
+
COMPLETED: Cleanup has finished, including if it failed. Inspect
|
|
21
|
+
the result or exception to determine the outcome.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
NOT_STARTED = "NOT_STARTED"
|
|
25
|
+
STARTED = "started"
|
|
26
|
+
CLEANUP = "cleanup"
|
|
27
|
+
COMPLETED = "completed"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _from_native(cls, native: _native.ShutdownPhase) -> "ShutdownPhase":
|
|
31
|
+
if native == _native.ShutdownPhase.NotStarted:
|
|
32
|
+
return cls.NOT_STARTED
|
|
33
|
+
if native == _native.ShutdownPhase.Started:
|
|
34
|
+
return cls.STARTED
|
|
35
|
+
if native == _native.ShutdownPhase.Cleanup:
|
|
36
|
+
return cls.CLEANUP
|
|
37
|
+
if native == _native.ShutdownPhase.Completed:
|
|
38
|
+
return cls.COMPLETED
|
|
39
|
+
raise ValueError(f"Unknown shutdown phase: {native!r}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ShutdownResult(Enum):
|
|
43
|
+
"""The outcome of completed shutdown.
|
|
44
|
+
|
|
45
|
+
Native cleanup failures raise ShutdownError.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
SUCCESS: Cleanup finished before the deadline.
|
|
49
|
+
TIMEOUT: The deadline expired before cleanup finished.
|
|
50
|
+
FAILED: A failure value; native failures raise ShutdownError instead.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
SUCCESS = "success"
|
|
54
|
+
TIMEOUT = "timeout"
|
|
55
|
+
FAILED = "failed"
|
|
56
|
+
|
|
57
|
+
def code(self) -> int:
|
|
58
|
+
"""Return 0 for success or 1 for any other outcome."""
|
|
59
|
+
return 0 if self is ShutdownResult.SUCCESS else 1
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def _from_native(cls, native: _native.ShutdownResult) -> "ShutdownResult":
|
|
63
|
+
if native == _native.ShutdownResult.Success:
|
|
64
|
+
return cls.SUCCESS
|
|
65
|
+
if native == _native.ShutdownResult.Timeout:
|
|
66
|
+
return cls.TIMEOUT
|
|
67
|
+
if native == _native.ShutdownResult.Failed:
|
|
68
|
+
raise ShutdownError("Encore resource cleanup failed")
|
|
69
|
+
|
|
70
|
+
raise ValueError(f"Unknown shutdown result: {native!r}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def begin(cooperative: bool = False) -> None:
|
|
74
|
+
"""Start shutdown and stop accepting new Pub/Sub messages.
|
|
75
|
+
|
|
76
|
+
All callers share one shutdown process. Calls are safe concurrently and
|
|
77
|
+
idempotent: the first call chooses the mode, and later calls do not restart
|
|
78
|
+
shutdown or reset its deadline.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
cooperative: Whether to wait for mark_application_drained() before
|
|
82
|
+
entering CLEANUP, unless the shutdown deadline expires first.
|
|
83
|
+
"""
|
|
84
|
+
runtime().begin_shutdown(cooperative=cooperative)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def completed() -> ShutdownResult:
|
|
88
|
+
"""Wait for actual cleanup completion, even after a timeout.
|
|
89
|
+
|
|
90
|
+
Call begin() first. Cancelling this wait does not cancel shutdown.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The shared shutdown outcome after cleanup has finished. A timeout
|
|
94
|
+
remains a timeout even if cleanup later finishes successfully.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ShutdownError: Shutdown has not begun, cleanup fails, or completion
|
|
98
|
+
cannot be observed.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
result = await runtime().completed()
|
|
102
|
+
except EncoreError as exc:
|
|
103
|
+
raise ShutdownError(str(exc)) from exc
|
|
104
|
+
return ShutdownResult._from_native(result)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def phase() -> ShutdownPhase:
|
|
108
|
+
"""Return the current shutdown phase, including before shutdown begins."""
|
|
109
|
+
return ShutdownPhase._from_native(runtime().phase())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def started() -> None:
|
|
113
|
+
"""Wait until shutdown has reached or passed STARTED.
|
|
114
|
+
|
|
115
|
+
Call begin() first. Cancelling this wait does not cancel shutdown.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ShutdownError: Shutdown has not begun or progress cannot be observed.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
await runtime().started()
|
|
122
|
+
except EncoreError as exc:
|
|
123
|
+
raise ShutdownError(str(exc)) from exc
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def cleanup() -> None:
|
|
127
|
+
"""Wait until shutdown has reached or passed CLEANUP.
|
|
128
|
+
|
|
129
|
+
Call begin() first. Cancelling this wait does not cancel shutdown.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ShutdownError: Shutdown has not begun or progress cannot be observed.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
await runtime().cleanup()
|
|
136
|
+
except EncoreError as exc:
|
|
137
|
+
raise ShutdownError(str(exc)) from exc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def force_cleanup() -> None:
|
|
141
|
+
"""Request CLEANUP without waiting for active work to drain.
|
|
142
|
+
|
|
143
|
+
Call begin() first. Repeated calls are harmless. Await cleanup() to
|
|
144
|
+
observe the transition and completed() to await completion. Active
|
|
145
|
+
handlers are not terminated and may lose access to closing resources.
|
|
146
|
+
Forcing cleanup alone does not change the result to TIMEOUT.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ShutdownError: Shutdown has not begun or cleanup cannot be forced.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
runtime().force_cleanup()
|
|
153
|
+
except EncoreError as exc:
|
|
154
|
+
raise ShutdownError(str(exc)) from exc
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def mark_application_drained() -> None:
|
|
158
|
+
"""Notify Encore that all application-initiated work has completed.
|
|
159
|
+
|
|
160
|
+
This includes work such as processing incoming HTTP requests. Call this
|
|
161
|
+
when begin(cooperative=True) was used to advance from STARTED to CLEANUP
|
|
162
|
+
without waiting for the deadline. Repeated calls are harmless.
|
|
163
|
+
"""
|
|
164
|
+
runtime().mark_application_drained()
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Object storage buckets declared at module scope and provisioned by Encore.
|
|
2
|
+
|
|
3
|
+
Use the bucket's async methods to upload, download, inspect, and remove objects.
|
|
4
|
+
Listing returns an async iterator; public URL construction is synchronous.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncGenerator, AsyncIterable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from ...internal.lazy import Lazy
|
|
13
|
+
from ...internal.runtime import _native, runtime
|
|
14
|
+
from ...tracing._context import active_ctx_bytes
|
|
15
|
+
|
|
16
|
+
ObjectsError = _native.ObjectsError
|
|
17
|
+
ObjectNotFound = _native.ObjectNotFound
|
|
18
|
+
PreconditionFailed = _native.PreconditionFailed
|
|
19
|
+
InvalidArgument = _native.InvalidArgument
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ObjectAttrs:
|
|
24
|
+
"""Metadata for a stored object.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
name: The object's logical key.
|
|
28
|
+
version: The stored version identifier, if available.
|
|
29
|
+
size: The object's size in bytes.
|
|
30
|
+
content_type: The MIME type, if available.
|
|
31
|
+
etag: The opaque entity tag.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
version: str | None
|
|
36
|
+
size: int
|
|
37
|
+
content_type: str | None
|
|
38
|
+
etag: str
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _from_native(cls, attrs: _native.ObjectAttrs) -> ObjectAttrs:
|
|
42
|
+
return cls(
|
|
43
|
+
attrs.name, attrs.version, attrs.size, attrs.content_type, attrs.etag
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ListEntry:
|
|
49
|
+
"""Metadata for an object returned by a bucket listing.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
name: The object's logical key.
|
|
53
|
+
size: The object's size in bytes.
|
|
54
|
+
etag: The opaque entity tag.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
size: int
|
|
59
|
+
etag: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_ttl(ttl: int) -> None:
|
|
63
|
+
if not 1 <= ttl <= 604800:
|
|
64
|
+
raise InvalidArgument("ttl must be between 1 and 604800 seconds")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Bucket:
|
|
68
|
+
"""An object-storage bucket.
|
|
69
|
+
|
|
70
|
+
Declare at module scope so Encore can discover and provision the bucket.
|
|
71
|
+
``versioned`` retains previous object versions; ``public`` makes objects
|
|
72
|
+
publicly accessible. Both default to False.
|
|
73
|
+
|
|
74
|
+
Storage failures raise ObjectsError or its subclasses. A missing object
|
|
75
|
+
raises ObjectNotFound, except for exists(), which returns False.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self, name: str, *, versioned: bool = False, public: bool = False
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Declare an object-storage bucket at module scope.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
name: The bucket's resource name.
|
|
85
|
+
versioned: Whether to retain previous object versions.
|
|
86
|
+
public: Whether objects are publicly accessible.
|
|
87
|
+
"""
|
|
88
|
+
self._name = name
|
|
89
|
+
self._versioned = versioned
|
|
90
|
+
self._public = public
|
|
91
|
+
self._handle = Lazy(lambda: runtime().bucket(name))
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def _impl(self) -> _native.Bucket:
|
|
95
|
+
return self._handle.get()
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def named(cls, name: str) -> Bucket:
|
|
99
|
+
"""Reference an existing bucket configured for this service by name."""
|
|
100
|
+
return cls(name)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
"""The declared bucket name."""
|
|
105
|
+
return self._name
|
|
106
|
+
|
|
107
|
+
async def upload(
|
|
108
|
+
self,
|
|
109
|
+
name: str,
|
|
110
|
+
data: bytes,
|
|
111
|
+
*,
|
|
112
|
+
content_type: str | None = None,
|
|
113
|
+
if_not_exists: bool = False,
|
|
114
|
+
) -> ObjectAttrs:
|
|
115
|
+
"""Upload bytes, replacing any existing object, and return metadata.
|
|
116
|
+
|
|
117
|
+
Versioned buckets retain previous versions.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
name: The object's logical key.
|
|
121
|
+
data: The bytes to store.
|
|
122
|
+
content_type: The optional MIME type.
|
|
123
|
+
if_not_exists: Whether to reject overwriting an existing object.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Metadata for the uploaded object.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
PreconditionFailed: The object exists and if_not_exists is True.
|
|
130
|
+
ObjectsError: The storage operation fails.
|
|
131
|
+
"""
|
|
132
|
+
options = _native.UploadOptions(
|
|
133
|
+
content_type=content_type,
|
|
134
|
+
preconditions=(
|
|
135
|
+
_native.UploadPreconditions(not_exists=True) if if_not_exists else None
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
attrs = await self._impl.object(name).upload(
|
|
139
|
+
data, options, source=active_ctx_bytes()
|
|
140
|
+
)
|
|
141
|
+
return ObjectAttrs._from_native(attrs)
|
|
142
|
+
|
|
143
|
+
async def upload_stream(
|
|
144
|
+
self,
|
|
145
|
+
name: str,
|
|
146
|
+
data: AsyncIterable[bytes],
|
|
147
|
+
*,
|
|
148
|
+
content_type: str | None = None,
|
|
149
|
+
if_not_exists: bool = False,
|
|
150
|
+
) -> ObjectAttrs:
|
|
151
|
+
"""Upload an asynchronous iterable of bytes with backpressure.
|
|
152
|
+
|
|
153
|
+
Chunks are consumed in order without buffering the whole object. Empty
|
|
154
|
+
chunks are allowed. Producer exceptions and cancellation abort the
|
|
155
|
+
upload; the caller owns and closes the input iterable, if necessary.
|
|
156
|
+
Like upload(), supports content_type and if_not_exists and returns the
|
|
157
|
+
stored object's metadata. Storage failures raise ObjectsError.
|
|
158
|
+
"""
|
|
159
|
+
options = _native.UploadOptions(
|
|
160
|
+
content_type=content_type,
|
|
161
|
+
preconditions=(
|
|
162
|
+
_native.UploadPreconditions(not_exists=True) if if_not_exists else None
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
stream = self._impl.object(name).upload_stream(
|
|
166
|
+
options, source=active_ctx_bytes()
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
async for chunk in data:
|
|
170
|
+
if not isinstance(chunk, bytes):
|
|
171
|
+
raise TypeError("upload_stream chunks must be bytes")
|
|
172
|
+
# Bound copies across the native boundary even if the producer
|
|
173
|
+
# hands us a very large chunk. Await each write for backpressure.
|
|
174
|
+
for offset in range(0, len(chunk), 65536):
|
|
175
|
+
await stream.write(chunk[offset : offset + 65536])
|
|
176
|
+
return ObjectAttrs._from_native(await stream.finish())
|
|
177
|
+
finally:
|
|
178
|
+
await stream.close()
|
|
179
|
+
|
|
180
|
+
async def download_stream(
|
|
181
|
+
self,
|
|
182
|
+
name: str,
|
|
183
|
+
*,
|
|
184
|
+
version: str | None = None,
|
|
185
|
+
offset: int = 0,
|
|
186
|
+
length: int | None = None,
|
|
187
|
+
) -> AsyncGenerator[bytes, None]:
|
|
188
|
+
"""Yield an object's bytes as chunks, without collecting it in memory.
|
|
189
|
+
|
|
190
|
+
Version and byte-range options have the same meaning as download().
|
|
191
|
+
Chunk sizes are provider-dependent. Consume with async for; when
|
|
192
|
+
stopping early, use contextlib.aclosing or call aclose() to release the
|
|
193
|
+
response promptly. Exhaustion, errors, and cancellation close it.
|
|
194
|
+
Storage failures raise ObjectsError, including ObjectNotFound.
|
|
195
|
+
"""
|
|
196
|
+
if offset < 0 or (length is not None and length <= 0):
|
|
197
|
+
raise InvalidArgument(
|
|
198
|
+
"offset must be nonnegative and length must be positive"
|
|
199
|
+
)
|
|
200
|
+
options = _native.DownloadOptions(
|
|
201
|
+
version=version,
|
|
202
|
+
range=(
|
|
203
|
+
_native.ByteRange(offset=offset, length=length)
|
|
204
|
+
if offset != 0 or length is not None
|
|
205
|
+
else None
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
stream = await self._impl.object(name).download_stream(
|
|
209
|
+
options, source=active_ctx_bytes()
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
while True:
|
|
213
|
+
chunk = await stream.next()
|
|
214
|
+
if chunk is None:
|
|
215
|
+
break
|
|
216
|
+
yield chunk
|
|
217
|
+
finally:
|
|
218
|
+
await stream.close()
|
|
219
|
+
|
|
220
|
+
async def download(
|
|
221
|
+
self,
|
|
222
|
+
name: str,
|
|
223
|
+
*,
|
|
224
|
+
version: str | None = None,
|
|
225
|
+
offset: int = 0,
|
|
226
|
+
length: int | None = None,
|
|
227
|
+
) -> bytes:
|
|
228
|
+
"""Download an object or byte range into memory.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
name: The object's logical key.
|
|
232
|
+
version: The stored version to read, or None for the latest version.
|
|
233
|
+
offset: The nonnegative, zero-based starting byte offset.
|
|
234
|
+
length: The positive maximum byte count, or None to read to the end.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
The requested object bytes.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
InvalidArgument: The range is invalid or starts beyond the object.
|
|
241
|
+
ObjectNotFound: The object or requested version does not exist.
|
|
242
|
+
ObjectsError: The storage operation fails.
|
|
243
|
+
"""
|
|
244
|
+
if offset < 0 or (length is not None and length <= 0):
|
|
245
|
+
raise InvalidArgument(
|
|
246
|
+
"offset must be nonnegative and length must be positive"
|
|
247
|
+
)
|
|
248
|
+
options = _native.DownloadOptions(
|
|
249
|
+
version=version,
|
|
250
|
+
range=(
|
|
251
|
+
_native.ByteRange(offset=offset, length=length)
|
|
252
|
+
if offset != 0 or length is not None
|
|
253
|
+
else None
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
return await self._impl.object(name).download_all(
|
|
257
|
+
options, source=active_ctx_bytes()
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def attrs(self, name: str, *, version: str | None = None) -> ObjectAttrs:
|
|
261
|
+
"""Return object metadata, defaulting to the latest version.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: The object's logical key.
|
|
265
|
+
version: The version to inspect, or None for the latest version.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ObjectNotFound: The object or requested version does not exist.
|
|
269
|
+
ObjectsError: The storage operation fails.
|
|
270
|
+
"""
|
|
271
|
+
attrs = await self._impl.object(name).attrs(
|
|
272
|
+
_native.AttrsOptions(version=version), source=active_ctx_bytes()
|
|
273
|
+
)
|
|
274
|
+
return ObjectAttrs._from_native(attrs)
|
|
275
|
+
|
|
276
|
+
async def exists(self, name: str, *, version: str | None = None) -> bool:
|
|
277
|
+
"""Return whether the object or selected version exists.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
name: The object's logical key.
|
|
281
|
+
version: The version to inspect, or None for the latest version.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
False if the object is missing, otherwise True.
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
ObjectsError: A storage failure other than a missing object occurs.
|
|
288
|
+
"""
|
|
289
|
+
return await self._impl.object(name).exists(
|
|
290
|
+
_native.ExistsOptions(version=version), source=active_ctx_bytes()
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def remove(self, name: str, *, version: str | None = None) -> None:
|
|
294
|
+
"""Remove an object or a selected version.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
name: The object's logical key.
|
|
298
|
+
version: The stored version to remove, or None for the default deletion.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ObjectsError: The storage operation fails.
|
|
302
|
+
"""
|
|
303
|
+
await self._impl.object(name).delete(
|
|
304
|
+
_native.DeleteOptions(version=version), source=active_ctx_bytes()
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def list(
|
|
308
|
+
self, *, prefix: str | None = None, limit: int | None = None
|
|
309
|
+
) -> AsyncGenerator[ListEntry, None]:
|
|
310
|
+
"""Yield object metadata matching the optional prefix and count limit.
|
|
311
|
+
|
|
312
|
+
Consume with async for. If stopping early, close the iterator with
|
|
313
|
+
aclose() or contextlib.aclosing to release it promptly.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
prefix: The key prefix to match, or None to include all keys.
|
|
317
|
+
limit: The maximum number of entries, or None for no limit. Zero
|
|
318
|
+
yields no entries.
|
|
319
|
+
|
|
320
|
+
Yields:
|
|
321
|
+
Metadata for each matching object.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
InvalidArgument: The limit is negative.
|
|
325
|
+
ObjectsError: The storage operation fails.
|
|
326
|
+
"""
|
|
327
|
+
if limit is not None and limit < 0:
|
|
328
|
+
raise InvalidArgument("limit must be nonnegative")
|
|
329
|
+
if limit == 0:
|
|
330
|
+
return
|
|
331
|
+
iterator = await self._impl.list(
|
|
332
|
+
_native.ListOptions(prefix=prefix, limit=limit), source=active_ctx_bytes()
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
while True:
|
|
336
|
+
entry = await iterator.next()
|
|
337
|
+
if entry is None:
|
|
338
|
+
break
|
|
339
|
+
yield ListEntry(entry.name, entry.size, entry.etag)
|
|
340
|
+
finally:
|
|
341
|
+
await iterator.mark_done()
|
|
342
|
+
|
|
343
|
+
async def signed_upload_url(self, name: str, *, ttl: int = 3600) -> str:
|
|
344
|
+
"""Return a URL granting temporary upload access to an object.
|
|
345
|
+
|
|
346
|
+
Anyone possessing the URL can upload without other authentication.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: The object's logical key.
|
|
350
|
+
ttl: The URL lifetime in seconds, from 1 to 604800 (seven days).
|
|
351
|
+
Defaults to 3600 (one hour).
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
The signed upload URL.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
InvalidArgument: The lifetime is outside the supported range.
|
|
358
|
+
ObjectsError: The storage operation fails.
|
|
359
|
+
"""
|
|
360
|
+
_validate_ttl(ttl)
|
|
361
|
+
return await self._impl.object(name).signed_upload_url(
|
|
362
|
+
_native.UploadUrlOptions(ttl=ttl), source=active_ctx_bytes()
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
async def signed_download_url(self, name: str, *, ttl: int = 3600) -> str:
|
|
366
|
+
"""Return a URL granting temporary download access to an object.
|
|
367
|
+
|
|
368
|
+
Anyone possessing the URL can download without other authentication.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
name: The object's logical key.
|
|
372
|
+
ttl: The URL lifetime in seconds, from 1 to 604800 (seven days).
|
|
373
|
+
Defaults to 3600 (one hour).
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
The signed download URL.
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
InvalidArgument: The lifetime is outside the supported range.
|
|
380
|
+
ObjectsError: The storage operation fails.
|
|
381
|
+
"""
|
|
382
|
+
_validate_ttl(ttl)
|
|
383
|
+
return await self._impl.object(name).signed_download_url(
|
|
384
|
+
_native.DownloadUrlOptions(ttl=ttl), source=active_ctx_bytes()
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def public_url(self, name: str) -> str:
|
|
388
|
+
"""Return an object's public URL.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name: The object's logical key.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ObjectsError: The bucket is private or URL construction fails.
|
|
395
|
+
"""
|
|
396
|
+
return self._impl.object(name).public_url()
|