objectstore-client 0.2.5__tar.gz → 0.2.7__tar.gz
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.
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/PKG-INFO +14 -1
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/README.md +13 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/pyproject.toml +1 -1
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/__init__.py +7 -1
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/client.py +90 -1
- objectstore_client-0.2.7/src/objectstore_client/errors.py +39 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/metadata.py +2 -2
- objectstore_client-0.2.5/src/objectstore_client/errors.py +0 -22
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/LICENSE.md +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/auth.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/metrics.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/multipart.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/presign.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/py.typed +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/scope.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/tracing.py +0 -0
- {objectstore_client-0.2.5 → objectstore_client-0.2.7}/src/objectstore_client/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: objectstore-client
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Client SDK for Objectstore, the Sentry object storage platform
|
|
5
5
|
Author: Sentry
|
|
6
6
|
Author-email: Sentry <oss@sentry.io>
|
|
@@ -183,6 +183,19 @@ usecase = Usecase("attachments", expiration_policy=TimeToIdle(timedelta(days=30)
|
|
|
183
183
|
session.put(b"payload", expiration_policy=TimeToLive(timedelta(hours=1)))
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
Use `Session.extend_expiry` to keep an existing object longer.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from objectstore_client import ExpiryExtensionRejected
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
session.extend_expiry(key, from_now=timedelta(days=30))
|
|
193
|
+
except ExpiryExtensionRejected:
|
|
194
|
+
print("Object could not be extended")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
See the `Session.extend_expiry` docs for target options and error handling.
|
|
198
|
+
|
|
186
199
|
### Origin Tracking
|
|
187
200
|
|
|
188
201
|
We encourage setting the `origin` on every upload to track where the payload was
|
|
@@ -64,6 +64,19 @@ usecase = Usecase("attachments", expiration_policy=TimeToIdle(timedelta(days=30)
|
|
|
64
64
|
session.put(b"payload", expiration_policy=TimeToLive(timedelta(hours=1)))
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Use `Session.extend_expiry` to keep an existing object longer.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from objectstore_client import ExpiryExtensionRejected
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
session.extend_expiry(key, from_now=timedelta(days=30))
|
|
74
|
+
except ExpiryExtensionRejected:
|
|
75
|
+
print("Object could not be extended")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See the `Session.extend_expiry` docs for target options and error handling.
|
|
79
|
+
|
|
67
80
|
### Origin Tracking
|
|
68
81
|
|
|
69
82
|
We encourage setting the `origin` on every upload to track where the payload was
|
|
@@ -5,7 +5,11 @@ from objectstore_client.client import (
|
|
|
5
5
|
Session,
|
|
6
6
|
Usecase,
|
|
7
7
|
)
|
|
8
|
-
from objectstore_client.errors import
|
|
8
|
+
from objectstore_client.errors import (
|
|
9
|
+
ExpiryExtensionRejected,
|
|
10
|
+
ObjectNotFound,
|
|
11
|
+
RequestError,
|
|
12
|
+
)
|
|
9
13
|
from objectstore_client.metadata import (
|
|
10
14
|
Compression,
|
|
11
15
|
ExpirationPolicy,
|
|
@@ -22,6 +26,8 @@ __all__ = [
|
|
|
22
26
|
"Session",
|
|
23
27
|
"GetResponse",
|
|
24
28
|
"RequestError",
|
|
29
|
+
"ObjectNotFound",
|
|
30
|
+
"ExpiryExtensionRejected",
|
|
25
31
|
"Compression",
|
|
26
32
|
"ExpirationPolicy",
|
|
27
33
|
"Metadata",
|
|
@@ -17,7 +17,12 @@ from urllib3.connectionpool import HTTPConnectionPool
|
|
|
17
17
|
|
|
18
18
|
from objectstore_client import presign, utils
|
|
19
19
|
from objectstore_client.auth import Permission, SecretKey, TokenProvider
|
|
20
|
-
from objectstore_client.errors import
|
|
20
|
+
from objectstore_client.errors import (
|
|
21
|
+
ExpiryExtensionRejected,
|
|
22
|
+
ObjectNotFound,
|
|
23
|
+
RequestError,
|
|
24
|
+
raise_for_status,
|
|
25
|
+
)
|
|
21
26
|
from objectstore_client.metadata import (
|
|
22
27
|
HEADER_EXPIRATION,
|
|
23
28
|
HEADER_FILENAME,
|
|
@@ -27,6 +32,7 @@ from objectstore_client.metadata import (
|
|
|
27
32
|
ExpirationPolicy,
|
|
28
33
|
Metadata,
|
|
29
34
|
format_expiration,
|
|
35
|
+
format_timedelta,
|
|
30
36
|
)
|
|
31
37
|
from objectstore_client.metrics import (
|
|
32
38
|
MetricsBackend,
|
|
@@ -671,6 +677,89 @@ class Session:
|
|
|
671
677
|
span.set_attribute("objectstore.found", True)
|
|
672
678
|
return Metadata.from_headers(response.headers)
|
|
673
679
|
|
|
680
|
+
def extend_expiry(
|
|
681
|
+
self,
|
|
682
|
+
key: str,
|
|
683
|
+
at: datetime | None = None,
|
|
684
|
+
from_creation: timedelta | None = None,
|
|
685
|
+
from_now: timedelta | None = None,
|
|
686
|
+
) -> None:
|
|
687
|
+
"""Extend an object's expiration deadline, preserving its payload.
|
|
688
|
+
|
|
689
|
+
Supply exactly one target: ``at`` is a timezone-aware absolute datetime;
|
|
690
|
+
``from_creation`` is total lifetime since creation or replacement; and
|
|
691
|
+
``from_now`` is lifetime from server request start. Relative targets are
|
|
692
|
+
resolved by the server, not added to the existing deadline. Fractional
|
|
693
|
+
duration seconds are truncated; absolute deadlines round upward to seconds.
|
|
694
|
+
|
|
695
|
+
An already-sufficient deadline succeeds without being shortened. Success
|
|
696
|
+
returns no value and does not indicate whether the deadline changed.
|
|
697
|
+
Requires object-write permission.
|
|
698
|
+
|
|
699
|
+
Actual extensions adjust TTL duration to approximately match the total
|
|
700
|
+
lifetime since creation; TTI duration remains unchanged.
|
|
701
|
+
|
|
702
|
+
Raises ``ValueError`` for missing or multiple targets, naive datetimes,
|
|
703
|
+
or negative durations. Zero durations are valid. Objects observed absent
|
|
704
|
+
or expired raise ``ObjectNotFound``. Rejected extensions raise
|
|
705
|
+
``ExpiryExtensionRejected`` for non-expiring or concurrently changed
|
|
706
|
+
objects, or missing creation metadata needed for a creation-relative
|
|
707
|
+
target. Both exceptions subclass ``RequestError``. Rejection does not
|
|
708
|
+
guarantee that the object still exists, since it may be deleted concurrently.
|
|
709
|
+
Other HTTP errors propagate normally. Retrying ``from_now`` establishes
|
|
710
|
+
a new server-time anchor and can extend the deadline further.
|
|
711
|
+
|
|
712
|
+
Example::
|
|
713
|
+
|
|
714
|
+
from objectstore_client import ExpiryExtensionRejected, ObjectNotFound
|
|
715
|
+
|
|
716
|
+
try:
|
|
717
|
+
session.extend_expiry(key, from_now=timedelta(days=30))
|
|
718
|
+
except ObjectNotFound:
|
|
719
|
+
print("Object is missing or expired")
|
|
720
|
+
except ExpiryExtensionRejected:
|
|
721
|
+
print("Extension was rejected")
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
if sum(value is not None for value in (at, from_creation, from_now)) != 1:
|
|
725
|
+
raise ValueError("Supply exactly one of at, from_creation, or from_now")
|
|
726
|
+
|
|
727
|
+
extension: dict[str, str]
|
|
728
|
+
if at is not None:
|
|
729
|
+
if at.utcoffset() is None:
|
|
730
|
+
raise ValueError("at must be a timezone-aware datetime")
|
|
731
|
+
extension = {"at": at.astimezone(UTC).isoformat()}
|
|
732
|
+
else:
|
|
733
|
+
delta = from_creation if from_creation is not None else from_now
|
|
734
|
+
assert delta is not None
|
|
735
|
+
if delta < timedelta(0):
|
|
736
|
+
raise ValueError("Expiry extension duration must not be negative")
|
|
737
|
+
extension = {
|
|
738
|
+
"after": format_timedelta(delta),
|
|
739
|
+
"from": "creation" if from_creation is not None else "now",
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
headers = self._make_headers()
|
|
743
|
+
with (
|
|
744
|
+
storage_span("extend_expiry", self._usecase, self._scope, key=key),
|
|
745
|
+
measure_storage_operation(
|
|
746
|
+
self._metrics_backend, "extend_expiry", self._usecase.name
|
|
747
|
+
),
|
|
748
|
+
):
|
|
749
|
+
response = self._pool.request(
|
|
750
|
+
"PATCH",
|
|
751
|
+
self._make_url(key),
|
|
752
|
+
headers=headers,
|
|
753
|
+
json={"extend_expiry": extension},
|
|
754
|
+
preload_content=True,
|
|
755
|
+
)
|
|
756
|
+
error_type: type[RequestError] = RequestError
|
|
757
|
+
if response.status == 404:
|
|
758
|
+
error_type = ObjectNotFound
|
|
759
|
+
elif response.status == 409:
|
|
760
|
+
error_type = ExpiryExtensionRejected
|
|
761
|
+
raise_for_status(response, error_type=error_type)
|
|
762
|
+
|
|
674
763
|
def delete(self, key: str) -> None:
|
|
675
764
|
"""
|
|
676
765
|
Deletes the blob with the given `key`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib3
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RequestError(Exception):
|
|
7
|
+
"""Exception raised if an API call to Objectstore fails."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status: int, response: str):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status = status
|
|
12
|
+
self.response = response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObjectNotFound(RequestError):
|
|
16
|
+
"""The object was observed to be absent or expired."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExpiryExtensionRejected(RequestError):
|
|
20
|
+
"""The object's expiration deadline could not be extended.
|
|
21
|
+
|
|
22
|
+
The object is non-expiring, lacks required creation metadata, or conflicts
|
|
23
|
+
with a conditional update. A conflict can result from concurrent deletion,
|
|
24
|
+
so this does not guarantee that the object still exists.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def raise_for_status(
|
|
29
|
+
response: urllib3.BaseHTTPResponse,
|
|
30
|
+
*,
|
|
31
|
+
error_type: type[RequestError] = RequestError,
|
|
32
|
+
) -> None:
|
|
33
|
+
if response.status >= 400:
|
|
34
|
+
res = (response.data or response.read() or b"").decode("utf-8", "replace")
|
|
35
|
+
raise error_type(
|
|
36
|
+
f"Objectstore request failed with status {response.status}",
|
|
37
|
+
response.status,
|
|
38
|
+
res,
|
|
39
|
+
)
|
|
@@ -53,8 +53,8 @@ class Metadata:
|
|
|
53
53
|
When using a Time To Idle expiration policy, this value will reflect the expiration
|
|
54
54
|
timestamp present prior to the current access to the object.
|
|
55
55
|
|
|
56
|
-
This field is computed by the server
|
|
57
|
-
|
|
56
|
+
This field is computed by the server. Use `expiration_policy` to set the
|
|
57
|
+
policy on upload, or `Session.extend_expiry` to extend an existing deadline.
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
origin: str | None
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import urllib3
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class RequestError(Exception):
|
|
7
|
-
"""Exception raised if an API call to Objectstore fails."""
|
|
8
|
-
|
|
9
|
-
def __init__(self, message: str, status: int, response: str):
|
|
10
|
-
super().__init__(message)
|
|
11
|
-
self.status = status
|
|
12
|
-
self.response = response
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def raise_for_status(response: urllib3.BaseHTTPResponse) -> None:
|
|
16
|
-
if response.status >= 400:
|
|
17
|
-
res = (response.data or response.read() or b"").decode("utf-8", "replace")
|
|
18
|
-
raise RequestError(
|
|
19
|
-
f"Objectstore request failed with status {response.status}",
|
|
20
|
-
response.status,
|
|
21
|
-
res,
|
|
22
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|