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.
Files changed (44) hide show
  1. encore/__init__.py +43 -0
  2. encore/cron/__init__.py +50 -0
  3. encore/internal/__init__.py +1 -0
  4. encore/internal/_loader.py +79 -0
  5. encore/internal/_native.pyi +734 -0
  6. encore/internal/lazy.py +35 -0
  7. encore/internal/runtime.py +133 -0
  8. encore/internal/startup.py +24 -0
  9. encore/logs/__init__.py +19 -0
  10. encore/logs/_console.py +155 -0
  11. encore/logs/_runtime.py +56 -0
  12. encore/meta/__init__.py +228 -0
  13. encore/metrics/__init__.py +390 -0
  14. encore/pubsub/__init__.py +425 -0
  15. encore/py.typed +0 -0
  16. encore/secrets/__init__.py +139 -0
  17. encore/shutdown/__init__.py +164 -0
  18. encore/storage/__init__.py +5 -0
  19. encore/storage/objects/__init__.py +396 -0
  20. encore/storage/postgres/__init__.py +737 -0
  21. encore/storage/redis/__init__.py +200 -0
  22. encore/storage/redis/_base.py +42 -0
  23. encore/storage/redis/_responses.py +150 -0
  24. encore/storage/redis/_values.py +189 -0
  25. encore/storage/redis/bitmap.py +142 -0
  26. encore/storage/redis/generic.py +367 -0
  27. encore/storage/redis/geo.py +508 -0
  28. encore/storage/redis/hash.py +504 -0
  29. encore/storage/redis/json.py +614 -0
  30. encore/storage/redis/list.py +511 -0
  31. encore/storage/redis/pubsub.py +276 -0
  32. encore/storage/redis/set.py +570 -0
  33. encore/storage/redis/sorted_set.py +753 -0
  34. encore/storage/redis/stream.py +773 -0
  35. encore/storage/redis/string.py +654 -0
  36. encore/tracing/__init__.py +19 -0
  37. encore/tracing/_context.py +27 -0
  38. encore/tracing/_encode.py +351 -0
  39. encore/tracing/_provider.py +62 -0
  40. encore/tracing/_span.py +226 -0
  41. encore/tracing/_tracer.py +93 -0
  42. encore_dev-2.0.0a7.dist-info/METADATA +791 -0
  43. encore_dev-2.0.0a7.dist-info/RECORD +44 -0
  44. 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)