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,200 @@
|
|
|
1
|
+
"""Redis databases and explicit read-only/read-write handles.
|
|
2
|
+
|
|
3
|
+
Declare Database(name) at module scope. Call commands directly on its families,
|
|
4
|
+
or pass read()/write() handles to other functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from ...internal.lazy import Lazy
|
|
12
|
+
from ...internal.runtime import _native, runtime
|
|
13
|
+
from . import generic as _generic
|
|
14
|
+
from . import string as _string
|
|
15
|
+
from . import bitmap as _bitmap
|
|
16
|
+
from . import hash as _hash
|
|
17
|
+
from . import json as _json
|
|
18
|
+
from . import list as _list
|
|
19
|
+
from . import set as _set
|
|
20
|
+
from . import sorted_set as _sorted_set
|
|
21
|
+
from . import geo as _geo
|
|
22
|
+
from . import stream as _stream
|
|
23
|
+
from . import pubsub as _pubsub
|
|
24
|
+
|
|
25
|
+
from ._values import RedisValue, Value
|
|
26
|
+
|
|
27
|
+
__all__ = ["Database", "DatabaseRef", "ReadOnlyDatabaseRef", "RedisValue", "Value"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReadOnlyDatabaseRef:
|
|
31
|
+
"""A handle exposing read-only command interfaces."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, get_handle: Callable[[], _native.RedisDatabase]) -> None:
|
|
34
|
+
"""Initialize a handle that resolves its connection on first use.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
get_handle: A factory returning the native database handle. Use
|
|
38
|
+
Database.read() to obtain a read-only handle in application
|
|
39
|
+
code.
|
|
40
|
+
"""
|
|
41
|
+
self._handle = Lazy(get_handle)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def generic(self) -> _generic.ReadOnlyInterface:
|
|
45
|
+
"""Read-only generic key commands for this handle."""
|
|
46
|
+
return _generic.ReadOnlyInterface(self._handle.get)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def string(self) -> _string.ReadOnlyInterface:
|
|
50
|
+
"""Read-only string commands for this handle."""
|
|
51
|
+
return _string.ReadOnlyInterface(self._handle.get)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def bitmap(self) -> _bitmap.ReadOnlyInterface:
|
|
55
|
+
"""Read-only bitmap commands for this handle."""
|
|
56
|
+
return _bitmap.ReadOnlyInterface(self._handle.get)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def hash(self) -> _hash.ReadOnlyInterface:
|
|
60
|
+
"""Read-only hash commands for this handle."""
|
|
61
|
+
return _hash.ReadOnlyInterface(self._handle.get)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def json(self) -> _json.ReadOnlyInterface:
|
|
65
|
+
"""Read-only RedisJSON commands for this handle."""
|
|
66
|
+
return _json.ReadOnlyInterface(self._handle.get)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def list(self) -> _list.ReadOnlyInterface:
|
|
70
|
+
"""Read-only list commands for this handle."""
|
|
71
|
+
return _list.ReadOnlyInterface(self._handle.get)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def set(self) -> _set.ReadOnlyInterface:
|
|
75
|
+
"""Read-only set commands for this handle."""
|
|
76
|
+
return _set.ReadOnlyInterface(self._handle.get)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def sorted_set(self) -> _sorted_set.ReadOnlyInterface:
|
|
80
|
+
"""Read-only sorted-set commands for this handle."""
|
|
81
|
+
return _sorted_set.ReadOnlyInterface(self._handle.get)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def geo(self) -> _geo.ReadOnlyInterface:
|
|
85
|
+
"""Read-only geospatial commands for this handle."""
|
|
86
|
+
return _geo.ReadOnlyInterface(self._handle.get)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def stream(self) -> _stream.ReadOnlyInterface:
|
|
90
|
+
"""Read-only stream commands for this handle."""
|
|
91
|
+
return _stream.ReadOnlyInterface(self._handle.get)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def pubsub(self) -> _pubsub.ReadOnlyInterface:
|
|
95
|
+
"""Read-only Redis channel commands for this handle."""
|
|
96
|
+
return _pubsub.ReadOnlyInterface(self._handle.get)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DatabaseRef(ReadOnlyDatabaseRef):
|
|
100
|
+
"""A handle exposing read and write command interfaces."""
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def connection_string(self) -> str:
|
|
104
|
+
"""The direct Redis server URL, including connection credentials.
|
|
105
|
+
|
|
106
|
+
External clients using this URL do not apply Encore key prefixes.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
EncoreError: The database connection cannot be initialized.
|
|
110
|
+
"""
|
|
111
|
+
return self._handle.get().connection_string()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def generic(self) -> _generic.Interface:
|
|
115
|
+
"""Read-write generic key commands for this handle."""
|
|
116
|
+
return _generic.Interface(self._handle.get)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def string(self) -> _string.Interface:
|
|
120
|
+
"""Read-write string commands for this handle."""
|
|
121
|
+
return _string.Interface(self._handle.get)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def bitmap(self) -> _bitmap.Interface:
|
|
125
|
+
"""Read-write bitmap commands for this handle."""
|
|
126
|
+
return _bitmap.Interface(self._handle.get)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def hash(self) -> _hash.Interface:
|
|
130
|
+
"""Read-write hash commands for this handle."""
|
|
131
|
+
return _hash.Interface(self._handle.get)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def json(self) -> _json.Interface:
|
|
135
|
+
"""Read-write RedisJSON commands for this handle."""
|
|
136
|
+
return _json.Interface(self._handle.get)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def list(self) -> _list.Interface:
|
|
140
|
+
"""Read-write list commands for this handle."""
|
|
141
|
+
return _list.Interface(self._handle.get)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def set(self) -> _set.Interface:
|
|
145
|
+
"""Read-write set commands for this handle."""
|
|
146
|
+
return _set.Interface(self._handle.get)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def sorted_set(self) -> _sorted_set.Interface:
|
|
150
|
+
"""Read-write sorted-set commands for this handle."""
|
|
151
|
+
return _sorted_set.Interface(self._handle.get)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def geo(self) -> _geo.Interface:
|
|
155
|
+
"""Read-write geospatial commands for this handle."""
|
|
156
|
+
return _geo.Interface(self._handle.get)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def stream(self) -> _stream.Interface:
|
|
160
|
+
"""Read-write stream commands for this handle."""
|
|
161
|
+
return _stream.Interface(self._handle.get)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def pubsub(self) -> _pubsub.Interface:
|
|
165
|
+
"""Read-write Redis channel commands for this handle."""
|
|
166
|
+
return _pubsub.Interface(self._handle.get)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class Database(DatabaseRef):
|
|
170
|
+
"""A named Redis database with read and write command interfaces.
|
|
171
|
+
|
|
172
|
+
Declare the database at module scope so Encore can discover it. Connections
|
|
173
|
+
are resolved lazily when a command is executed.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, name: str) -> None:
|
|
177
|
+
"""Declare a Redis database resource.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: The database's logical name in the Encore application.
|
|
181
|
+
"""
|
|
182
|
+
self._name = name
|
|
183
|
+
super().__init__(lambda: runtime().redis_database(name))
|
|
184
|
+
|
|
185
|
+
def write(self) -> DatabaseRef:
|
|
186
|
+
"""Return a handle granting read-write access to this database."""
|
|
187
|
+
return DatabaseRef(lambda: self._handle.get().write())
|
|
188
|
+
|
|
189
|
+
def read(self, primary: bool = False) -> ReadOnlyDatabaseRef:
|
|
190
|
+
"""Return a read-only handle.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
primary: Whether to request the primary server even when replicas
|
|
194
|
+
are available. Currently all reads use the primary, including
|
|
195
|
+
when this is False.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
A lazy handle exposing only read-only command interfaces.
|
|
199
|
+
"""
|
|
200
|
+
return ReadOnlyDatabaseRef(lambda: self._handle.get().read(primary=primary))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared lazy native-handle access for Redis command families."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Generic
|
|
5
|
+
|
|
6
|
+
from ._values import T, validate_decoder
|
|
7
|
+
|
|
8
|
+
from ...internal.runtime import _native
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandInterface:
|
|
12
|
+
"""A command family backed by a lazily resolved native database handle."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, get_handle: Callable[[], _native.RedisDatabase]) -> None:
|
|
15
|
+
"""Initialize a command family without opening a connection.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
get_handle: A factory returning the native database handle.
|
|
19
|
+
"""
|
|
20
|
+
self._get_handle = get_handle
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TypedCommand(CommandInterface, Generic[T]):
|
|
24
|
+
"""A lazy command bound to a value decoder, selected with command[T]."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
get_handle: Callable[[], _native.RedisDatabase],
|
|
29
|
+
value_type: type[T],
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Bind a lazy command to a value decoder.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
get_handle: A factory returning the native database handle when
|
|
35
|
+
called.
|
|
36
|
+
value_type: bytes, str, int, float, or a class implementing
|
|
37
|
+
RedisValue. The type selects how stored values are decoded;
|
|
38
|
+
command metadata is unchanged.
|
|
39
|
+
"""
|
|
40
|
+
validate_decoder(value_type)
|
|
41
|
+
super().__init__(get_handle)
|
|
42
|
+
self._value_type = value_type
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Decode values in raw RESP replies without converting command metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._values import T, decode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fields(data: Any, as_type: type[T]) -> Any:
|
|
11
|
+
"""Decode hash or stream values while preserving field names.
|
|
12
|
+
|
|
13
|
+
Decoding errors propagate to the caller.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
data: A flat alternating field/value list, nested field/value pairs, a
|
|
17
|
+
field-to-value dictionary, or None.
|
|
18
|
+
as_type: A validated value decoder.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The corresponding field/value layout with decoded values. None and the
|
|
22
|
+
default bytes mode are preserved.
|
|
23
|
+
"""
|
|
24
|
+
if data is None or as_type is bytes:
|
|
25
|
+
return data
|
|
26
|
+
if isinstance(data, dict):
|
|
27
|
+
return {key: decode(value, as_type) for key, value in data.items()}
|
|
28
|
+
if data and isinstance(data[0], (list, tuple)):
|
|
29
|
+
return [[key, decode(value, as_type)] for key, value in data]
|
|
30
|
+
return [
|
|
31
|
+
decode(value, as_type) if index % 2 else value
|
|
32
|
+
for index, value in enumerate(data)
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def entries(data: Any, as_type: type[T]) -> Any:
|
|
37
|
+
"""Decode stream entry values while preserving IDs and field names.
|
|
38
|
+
|
|
39
|
+
Decoding errors propagate to the caller.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
data: A list of [entry ID, fields] pairs, or None. Fields may be None
|
|
43
|
+
for deleted pending entries.
|
|
44
|
+
as_type: A validated value decoder.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A list of entries with decoded field values. Entry IDs, field names, and
|
|
48
|
+
missing fields are preserved.
|
|
49
|
+
"""
|
|
50
|
+
if data is None or as_type is bytes:
|
|
51
|
+
return data
|
|
52
|
+
return [[entry_id, fields(values, as_type)] for entry_id, values in data]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def streams(data: Any, as_type: type[T]) -> Any:
|
|
56
|
+
"""Decode stream values while preserving stream keys and reply layout.
|
|
57
|
+
|
|
58
|
+
Decoding errors propagate to the caller.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
data: An XREAD or XREADGROUP reply: a stream-key dictionary in RESP3, a
|
|
62
|
+
list of [stream key, entries] pairs in RESP2, or None.
|
|
63
|
+
as_type: A validated value decoder.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The same protocol layout with decoded field values. Stream keys, entry
|
|
67
|
+
IDs, field names, and None are preserved.
|
|
68
|
+
"""
|
|
69
|
+
if data is None or as_type is bytes:
|
|
70
|
+
return data
|
|
71
|
+
if isinstance(data, dict):
|
|
72
|
+
return {key: entries(values, as_type) for key, values in data.items()}
|
|
73
|
+
return [[key, entries(values, as_type)] for key, values in data]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def autoclaim(data: Any, as_type: type[T], justid: bool) -> Any:
|
|
77
|
+
"""Decode claimed stream entries while preserving claim metadata.
|
|
78
|
+
|
|
79
|
+
Decoding errors propagate to the caller.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
data: An XAUTOCLAIM reply containing the next scan ID, claimed entries,
|
|
83
|
+
and optionally deleted entry IDs.
|
|
84
|
+
as_type: A validated value decoder.
|
|
85
|
+
justid: Whether the reply contains entry IDs instead of entries.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The reply with decoded entry values. Scan IDs and deleted IDs are
|
|
89
|
+
unchanged; ID-only replies are returned unchanged.
|
|
90
|
+
"""
|
|
91
|
+
if justid or data is None or as_type is bytes:
|
|
92
|
+
return data
|
|
93
|
+
return [data[0], entries(data[1], as_type), *data[2:]]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def stream_info(data: Any, as_type: type[T]) -> Any:
|
|
97
|
+
"""Decode values in stream information without changing metadata.
|
|
98
|
+
|
|
99
|
+
Decoding errors propagate to the caller.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
data: An XINFO STREAM reply: a metadata dictionary in RESP3 or an
|
|
103
|
+
alternating key/value list in RESP2.
|
|
104
|
+
as_type: A validated value decoder.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The same layout with values decoded in first-entry, last-entry, and
|
|
108
|
+
entries. All other metadata is unchanged.
|
|
109
|
+
"""
|
|
110
|
+
if data is None or as_type is bytes:
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
def convert(key: bytes, value: Any) -> Any:
|
|
114
|
+
"""Decode only metadata fields containing stored stream entries."""
|
|
115
|
+
if key in (b"first-entry", b"last-entry"):
|
|
116
|
+
return None if value is None else entries([value], as_type)[0]
|
|
117
|
+
if key == b"entries":
|
|
118
|
+
return entries(value, as_type)
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
if isinstance(data, dict):
|
|
122
|
+
return {key: convert(key, value) for key, value in data.items()}
|
|
123
|
+
return [
|
|
124
|
+
convert(data[index - 1], value) if index % 2 else value
|
|
125
|
+
for index, value in enumerate(data)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def geo_members(data: Any, as_type: type[T]) -> Any:
|
|
130
|
+
"""Decode geospatial members without changing result metadata.
|
|
131
|
+
|
|
132
|
+
Decoding errors propagate to the caller.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
data: A list of members or rows starting with a member. Store variants
|
|
136
|
+
may instead return a count.
|
|
137
|
+
as_type: A validated value decoder.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Decoded members, with any distances, coordinates, and hashes unchanged.
|
|
141
|
+
Counts and other non-list replies are returned unchanged.
|
|
142
|
+
"""
|
|
143
|
+
if not isinstance(data, list) or as_type is bytes:
|
|
144
|
+
return data
|
|
145
|
+
return [
|
|
146
|
+
[decode(row[0], as_type), *row[1:]]
|
|
147
|
+
if isinstance(row, list)
|
|
148
|
+
else decode(row, as_type)
|
|
149
|
+
for row in data
|
|
150
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Encoding and explicit per-command decoding of Redis values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, TypeVar, Union, cast
|
|
6
|
+
|
|
7
|
+
_R = TypeVar("_R", bound="RedisValue")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RedisValue(Protocol):
|
|
11
|
+
"""A value that can serialize itself to Redis and reconstruct itself.
|
|
12
|
+
|
|
13
|
+
Implementing these methods is sufficient; inheritance is not required.
|
|
14
|
+
Encoding must return bytes. Decoding errors propagate to the caller.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def encode(self) -> bytes:
|
|
18
|
+
"""Serialize this value for storage in Redis.
|
|
19
|
+
|
|
20
|
+
The SDK calls this method when writing a custom value. Encoding errors
|
|
21
|
+
propagate to the caller before the command is sent.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The bytes to store. The class's decode() method must be able to
|
|
25
|
+
reconstruct the value from these bytes.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def decode(cls: type[_R], data: bytes) -> _R:
|
|
31
|
+
"""Reconstruct a value from its stored representation.
|
|
32
|
+
|
|
33
|
+
The SDK calls this method for reads such as db.string.get[MyValue](key).
|
|
34
|
+
Raise an exception if the bytes cannot be decoded; decoding errors
|
|
35
|
+
propagate to the caller rather than being treated as missing values.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
data: The stored representation produced by encode().
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
An instance of the class on which decode() was called.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Union (rather than |) keeps this runtime type alias compatible with Python 3.9.
|
|
47
|
+
Value = Union[int, float, str, bytes, RedisValue]
|
|
48
|
+
T = TypeVar("T", bound=Value)
|
|
49
|
+
U = TypeVar("U", bound=Value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def encode(value: Value) -> bytes:
|
|
53
|
+
"""Encode a value for a Redis command.
|
|
54
|
+
|
|
55
|
+
Floats use Python's repr() as ASCII text, including nan and infinities.
|
|
56
|
+
Redis numeric commands may reject non-finite values. Custom encoding errors
|
|
57
|
+
propagate before the command is sent.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: Bytes, UTF-8 text, an integer, a float, or a RedisValue
|
|
61
|
+
implementation. Booleans are not supported. Custom encoders must
|
|
62
|
+
return bytes.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Bytes unchanged, UTF-8-encoded text, ASCII numeric text, or the
|
|
66
|
+
custom encoder's result.
|
|
67
|
+
"""
|
|
68
|
+
if isinstance(value, bytes):
|
|
69
|
+
return value
|
|
70
|
+
if isinstance(value, str):
|
|
71
|
+
return value.encode("utf-8")
|
|
72
|
+
if isinstance(value, bool):
|
|
73
|
+
raise TypeError(
|
|
74
|
+
"Redis values must be bytes, str, int, float, or RedisValue; not bool"
|
|
75
|
+
)
|
|
76
|
+
if isinstance(value, int):
|
|
77
|
+
return str(value).encode("ascii")
|
|
78
|
+
if isinstance(value, float):
|
|
79
|
+
return repr(value).encode("ascii")
|
|
80
|
+
encoder = getattr(value, "encode", None)
|
|
81
|
+
if not callable(encoder):
|
|
82
|
+
raise TypeError("Redis values must be bytes, str, int, float, or RedisValue")
|
|
83
|
+
result = encoder()
|
|
84
|
+
if not isinstance(result, bytes):
|
|
85
|
+
raise TypeError("RedisValue.encode() must return bytes")
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def decode(data: bytes, as_type: type[T]) -> T:
|
|
90
|
+
"""Decode one stored value using a validated decoder.
|
|
91
|
+
|
|
92
|
+
Custom decoding errors propagate to the caller. This helper does not decode
|
|
93
|
+
keys, field names, or other command metadata.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
data: The stored value.
|
|
97
|
+
as_type: bytes, str, int, float, or a class implementing RedisValue.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The value decoded as as_type; bytes are returned unchanged.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
UnicodeDecodeError: The stored value is not UTF-8 when as_type is str.
|
|
104
|
+
ValueError: The stored value cannot be parsed when as_type is int or
|
|
105
|
+
float.
|
|
106
|
+
"""
|
|
107
|
+
if as_type is bytes:
|
|
108
|
+
return cast(T, data)
|
|
109
|
+
if as_type is str:
|
|
110
|
+
return cast(T, data.decode("utf-8"))
|
|
111
|
+
if as_type is int:
|
|
112
|
+
return cast(T, int(data))
|
|
113
|
+
if as_type is float:
|
|
114
|
+
return cast(T, float(data))
|
|
115
|
+
return cast(T, cast("type[RedisValue]", as_type).decode(data))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def validate_decoder(as_type: type[T]) -> None:
|
|
119
|
+
"""Validate a decoder before a command can affect Redis.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
as_type: The proposed runtime decoder class.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
TypeError: The decoder is not bytes, str, int, float, or a class with a
|
|
126
|
+
callable decode() method.
|
|
127
|
+
"""
|
|
128
|
+
if as_type in (bytes, str, int, float):
|
|
129
|
+
return
|
|
130
|
+
if not isinstance(as_type, type) or not callable(getattr(as_type, "decode", None)):
|
|
131
|
+
raise TypeError(
|
|
132
|
+
"The type argument must be bytes, str, int, float, or a RedisValue class"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def decode_optional(data: bytes | None, as_type: type[T]) -> T | None:
|
|
137
|
+
"""Decode a stored value, preserving None for a missing value.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
data: A stored value or None.
|
|
141
|
+
as_type: A validated value decoder.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The decoded value, or None without invoking the decoder.
|
|
145
|
+
"""
|
|
146
|
+
return None if data is None else decode(data, as_type)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def decode_pop(
|
|
150
|
+
data: bytes | list[bytes] | None, as_type: type[T]
|
|
151
|
+
) -> T | list[T] | None:
|
|
152
|
+
"""Decode a single value or collection returned by a pop command.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
data: A single value, a list of values, or None for a missing key.
|
|
156
|
+
as_type: A validated value decoder.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A decoded value, a list of decoded values, or None, matching the input
|
|
160
|
+
shape. Empty lists stay empty. Decoding errors propagate.
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(data, list):
|
|
163
|
+
return [decode(value, as_type) for value in data]
|
|
164
|
+
return decode_optional(data, as_type)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def decode_sorted(
|
|
168
|
+
data: bytes | list[bytes] | list[tuple[bytes, float]] | None,
|
|
169
|
+
as_type: type[T],
|
|
170
|
+
) -> T | list[T] | list[tuple[T, float]] | None:
|
|
171
|
+
"""Decode sorted-set members while preserving scores and reply shape.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
data: A member, a list of members, a list of (member, score) tuples, or
|
|
175
|
+
None for a missing key.
|
|
176
|
+
as_type: A validated member decoder.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The same reply shape with decoded members and unchanged numeric scores.
|
|
180
|
+
Empty lists and None are preserved. Decoding errors propagate.
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(data, list):
|
|
183
|
+
if data and isinstance(data[0], tuple):
|
|
184
|
+
return [
|
|
185
|
+
(decode(member, as_type), score)
|
|
186
|
+
for member, score in cast("list[tuple[bytes, float]]", data)
|
|
187
|
+
]
|
|
188
|
+
return [decode(member, as_type) for member in cast("list[bytes]", data)]
|
|
189
|
+
return decode_optional(data, as_type)
|