objectstore-client 0.2.0__tar.gz → 0.2.1__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.0 → objectstore_client-0.2.1}/PKG-INFO +1 -1
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/pyproject.toml +1 -1
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/client.py +6 -6
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/metadata.py +21 -3
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/utils.py +32 -1
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/LICENSE.md +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/README.md +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/__init__.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/auth.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/errors.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/metrics.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/multipart.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/presign.py +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/py.typed +0 -0
- {objectstore_client-0.2.0 → objectstore_client-0.2.1}/src/objectstore_client/scope.py +0 -0
|
@@ -354,14 +354,14 @@ class Session:
|
|
|
354
354
|
headers[HEADER_EXPIRATION] = format_expiration(expiration_policy)
|
|
355
355
|
|
|
356
356
|
if origin:
|
|
357
|
-
headers[HEADER_ORIGIN] = origin
|
|
357
|
+
headers[HEADER_ORIGIN] = utils.encode_header_value(origin)
|
|
358
358
|
|
|
359
359
|
if filename is not None:
|
|
360
|
-
headers[HEADER_FILENAME] = filename
|
|
360
|
+
headers[HEADER_FILENAME] = utils.encode_header_value(filename)
|
|
361
361
|
|
|
362
362
|
if metadata:
|
|
363
363
|
for k, v in metadata.items():
|
|
364
|
-
headers[f"{HEADER_META_PREFIX}{k}"] = v
|
|
364
|
+
headers[f"{HEADER_META_PREFIX}{k}"] = utils.encode_header_value(v)
|
|
365
365
|
|
|
366
366
|
if key == "":
|
|
367
367
|
key = None
|
|
@@ -619,14 +619,14 @@ class Session:
|
|
|
619
619
|
headers[HEADER_EXPIRATION] = format_expiration(expiration_policy)
|
|
620
620
|
|
|
621
621
|
if origin:
|
|
622
|
-
headers[HEADER_ORIGIN] = origin
|
|
622
|
+
headers[HEADER_ORIGIN] = utils.encode_header_value(origin)
|
|
623
623
|
|
|
624
624
|
if filename is not None:
|
|
625
|
-
headers[HEADER_FILENAME] = filename
|
|
625
|
+
headers[HEADER_FILENAME] = utils.encode_header_value(filename)
|
|
626
626
|
|
|
627
627
|
if metadata:
|
|
628
628
|
for k, v in metadata.items():
|
|
629
|
-
headers[f"{HEADER_META_PREFIX}{k}"] = v
|
|
629
|
+
headers[f"{HEADER_META_PREFIX}{k}"] = utils.encode_header_value(v)
|
|
630
630
|
|
|
631
631
|
if key == "":
|
|
632
632
|
key = None
|
|
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
8
|
from typing import Literal, TypeVar, cast
|
|
9
9
|
|
|
10
|
+
from objectstore_client.utils import decode_header_value
|
|
11
|
+
|
|
10
12
|
Compression = Literal["zstd"] | Literal["none"]
|
|
11
13
|
|
|
12
14
|
HEADER_EXPIRATION = "x-sn-expiration"
|
|
@@ -14,6 +16,7 @@ HEADER_TIME_CREATED = "x-sn-time-created"
|
|
|
14
16
|
HEADER_TIME_EXPIRES = "x-sn-time-expires"
|
|
15
17
|
HEADER_ORIGIN = "x-sn-origin"
|
|
16
18
|
HEADER_FILENAME = "x-sn-filename"
|
|
19
|
+
HEADER_SIZE = "x-sn-size"
|
|
17
20
|
HEADER_META_PREFIX = "x-snme-"
|
|
18
21
|
|
|
19
22
|
|
|
@@ -70,6 +73,17 @@ class Metadata:
|
|
|
70
73
|
prompting browsers and download tools to save the file under this name.
|
|
71
74
|
"""
|
|
72
75
|
|
|
76
|
+
size: int | None
|
|
77
|
+
"""
|
|
78
|
+
The size of the complete stored object in bytes.
|
|
79
|
+
|
|
80
|
+
This is always the size of the whole object, even when only a range of it was
|
|
81
|
+
requested. For a compressed object it is the compressed size, matching the bytes on
|
|
82
|
+
the wire.
|
|
83
|
+
|
|
84
|
+
This field is computed by the server, it cannot be set by clients.
|
|
85
|
+
"""
|
|
86
|
+
|
|
73
87
|
custom: dict[str, str]
|
|
74
88
|
|
|
75
89
|
@classmethod
|
|
@@ -81,6 +95,7 @@ class Metadata:
|
|
|
81
95
|
time_expires = None
|
|
82
96
|
origin = None
|
|
83
97
|
filename = None
|
|
98
|
+
size = None
|
|
84
99
|
custom_metadata = {}
|
|
85
100
|
|
|
86
101
|
for k, v in headers.items():
|
|
@@ -95,11 +110,13 @@ class Metadata:
|
|
|
95
110
|
elif k == HEADER_TIME_EXPIRES:
|
|
96
111
|
time_expires = datetime.fromisoformat(v)
|
|
97
112
|
elif k == HEADER_ORIGIN:
|
|
98
|
-
origin = v
|
|
113
|
+
origin = decode_header_value(v)
|
|
99
114
|
elif k == HEADER_FILENAME:
|
|
100
|
-
filename = v
|
|
115
|
+
filename = decode_header_value(v)
|
|
116
|
+
elif k == HEADER_SIZE:
|
|
117
|
+
size = int(v)
|
|
101
118
|
elif k.startswith(HEADER_META_PREFIX):
|
|
102
|
-
custom_metadata[k[len(HEADER_META_PREFIX) :]] = v
|
|
119
|
+
custom_metadata[k[len(HEADER_META_PREFIX) :]] = decode_header_value(v)
|
|
103
120
|
|
|
104
121
|
return Metadata(
|
|
105
122
|
content_type=content_type,
|
|
@@ -109,6 +126,7 @@ class Metadata:
|
|
|
109
126
|
time_expires=time_expires,
|
|
110
127
|
origin=origin,
|
|
111
128
|
filename=filename,
|
|
129
|
+
size=size,
|
|
112
130
|
custom=custom_metadata,
|
|
113
131
|
)
|
|
114
132
|
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
from typing import IO, Any
|
|
5
|
-
from urllib.parse import quote
|
|
5
|
+
from urllib.parse import quote, unquote
|
|
6
6
|
|
|
7
7
|
import filetype # type: ignore[import-untyped]
|
|
8
8
|
from zstandard import ZstdCompressionReader
|
|
@@ -22,6 +22,16 @@ from zstandard import ZstdCompressionReader
|
|
|
22
22
|
_PATH_SAFE = "/:@!$&'()*+,;="
|
|
23
23
|
_QUERY_SAFE = _PATH_SAFE + "?"
|
|
24
24
|
|
|
25
|
+
# Characters left unescaped in a metadata header value: every visible ASCII
|
|
26
|
+
# character except "%". Non-ASCII, the C0 controls, and DEL are escaped because a
|
|
27
|
+
# header value cannot carry them; "%" is escaped so that a literal percent sign in
|
|
28
|
+
# a value can never be confused with an escape sequence when decoding.
|
|
29
|
+
#
|
|
30
|
+
# This is byte-for-byte identical to the Rust `HEADER_ESCAPE` set (see
|
|
31
|
+
# `objectstore-types/src/headers.rs`), so both clients and the server agree on the
|
|
32
|
+
# wire form. Values that are already plain ASCII are left untouched.
|
|
33
|
+
_HEADER_VALUE_SAFE = "".join(chr(b) for b in range(0x20, 0x7F) if b != ord("%"))
|
|
34
|
+
|
|
25
35
|
|
|
26
36
|
def encode_path(path: str) -> str:
|
|
27
37
|
"""Percent-encodes a request path as it will appear on the wire.
|
|
@@ -42,6 +52,27 @@ def encode_query(query: str) -> str:
|
|
|
42
52
|
return quote(query, safe=_QUERY_SAFE)
|
|
43
53
|
|
|
44
54
|
|
|
55
|
+
def encode_header_value(value: str) -> str:
|
|
56
|
+
"""Percent-encodes a free-form metadata string for an HTTP header value.
|
|
57
|
+
|
|
58
|
+
HTTP header values carry only visible ASCII, so anything outside that range has
|
|
59
|
+
to be escaped to survive the transport (see :data:`_HEADER_VALUE_SAFE`). This is
|
|
60
|
+
the inverse of :func:`decode_header_value`; only the transport representation is
|
|
61
|
+
escaped, never the logical string held in :class:`Metadata`.
|
|
62
|
+
"""
|
|
63
|
+
return quote(value, safe=_HEADER_VALUE_SAFE)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def decode_header_value(value: str) -> str:
|
|
67
|
+
"""Decodes a percent-encoded HTTP header value into its logical string.
|
|
68
|
+
|
|
69
|
+
Inverse of :func:`encode_header_value`. Values without escape sequences are
|
|
70
|
+
returned unchanged, so headers written before the encoding existed still read
|
|
71
|
+
back as-is.
|
|
72
|
+
"""
|
|
73
|
+
return unquote(value, errors="strict")
|
|
74
|
+
|
|
75
|
+
|
|
45
76
|
def parse_accept_encoding(header: str) -> list[str]:
|
|
46
77
|
"""Parse an Accept-Encoding header value for use in objectstore GET requests.
|
|
47
78
|
|
|
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
|
|
File without changes
|