flo-python 0.1.0.dev4__py3-none-any.whl → 0.1.2__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.
- flo/__init__.py +60 -15
- flo/actions.py +20 -27
- flo/client.py +193 -46
- flo/exceptions.py +83 -23
- flo/kv.py +373 -165
- flo/kv_body.py +554 -0
- flo/kv_txn.py +237 -0
- flo/processing.py +3 -67
- flo/queue.py +25 -101
- flo/retry.py +124 -0
- flo/streams.py +146 -31
- flo/types.py +431 -221
- flo/wire.py +263 -208
- flo/worker.py +684 -250
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/METADATA +165 -98
- flo_python-0.1.2.dist-info/RECORD +19 -0
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/WHEEL +1 -1
- flo_python-0.1.2.dist-info/licenses/LICENSE +202 -0
- flo_python-0.1.0.dev4.dist-info/RECORD +0 -16
- flo_python-0.1.0.dev4.dist-info/licenses/LICENSE +0 -21
flo/kv.py
CHANGED
|
@@ -3,24 +3,172 @@
|
|
|
3
3
|
Key-value store operations for Flo client.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from .exceptions import IncompleteResponseError, ServerError, raise_for_status
|
|
10
|
+
from .kv_body import (
|
|
11
|
+
decode_delete_answer,
|
|
12
|
+
decode_empty_answer,
|
|
13
|
+
decode_exists_answer,
|
|
14
|
+
decode_get_answer,
|
|
15
|
+
decode_history_answer,
|
|
16
|
+
decode_incr_answer,
|
|
17
|
+
decode_json_del_answer,
|
|
18
|
+
decode_json_get_answer,
|
|
19
|
+
decode_json_set_answer,
|
|
20
|
+
decode_mget_answer,
|
|
21
|
+
decode_scan_answer,
|
|
22
|
+
decode_set_answer,
|
|
23
|
+
encode_delete_body,
|
|
24
|
+
encode_exists_body,
|
|
25
|
+
encode_expire_body,
|
|
26
|
+
encode_get_body,
|
|
27
|
+
encode_history_body,
|
|
28
|
+
encode_incr_body,
|
|
29
|
+
encode_json_del_body,
|
|
30
|
+
encode_json_get_body,
|
|
31
|
+
encode_json_set_body,
|
|
32
|
+
encode_mget_body,
|
|
33
|
+
encode_scan_body,
|
|
34
|
+
encode_set_body,
|
|
35
|
+
split_mget_keys,
|
|
36
|
+
)
|
|
9
37
|
from .types import (
|
|
10
38
|
DeleteOptions,
|
|
39
|
+
GetJsonResult,
|
|
11
40
|
GetOptions,
|
|
41
|
+
GetResult,
|
|
12
42
|
HistoryOptions,
|
|
43
|
+
IncrResult,
|
|
44
|
+
KVExistsOptions,
|
|
45
|
+
KVExpireOptions,
|
|
46
|
+
KVIncrOptions,
|
|
47
|
+
KVJsonOptions,
|
|
48
|
+
KVMGetOptions,
|
|
49
|
+
MGetEntry,
|
|
13
50
|
OpCode,
|
|
14
|
-
OptionTag,
|
|
15
51
|
PutOptions,
|
|
52
|
+
PutResult,
|
|
53
|
+
Reason,
|
|
16
54
|
ScanOptions,
|
|
17
55
|
ScanResult,
|
|
18
56
|
VersionEntry,
|
|
19
57
|
)
|
|
20
|
-
from .wire import
|
|
58
|
+
from .wire import RawResponse
|
|
21
59
|
|
|
22
60
|
if TYPE_CHECKING:
|
|
23
61
|
from .client import FloClient
|
|
62
|
+
from .kv_txn import Transaction
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _key(key: str | bytes) -> bytes:
|
|
66
|
+
return key.encode("utf-8") if isinstance(key, str) else key
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _absent(response: RawResponse) -> bool:
|
|
70
|
+
"""Whether a not-found answer says the thing is absent; any other not-found
|
|
71
|
+
(no such transaction) is raised."""
|
|
72
|
+
if not response.is_not_found():
|
|
73
|
+
return False
|
|
74
|
+
try:
|
|
75
|
+
raise_for_status(response.status, response.data)
|
|
76
|
+
except ServerError as e:
|
|
77
|
+
if e.reason == Reason.NOT_FOUND:
|
|
78
|
+
return True
|
|
79
|
+
raise
|
|
80
|
+
return False # pragma: no cover - raise_for_status always raises here
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def kv_get(
|
|
84
|
+
client: "FloClient", namespace: str, key: str | bytes, opts: GetOptions, txn: int | None = None
|
|
85
|
+
) -> GetResult | None:
|
|
86
|
+
"""kv.get, inside transaction ``txn`` when given."""
|
|
87
|
+
response = await client._send_and_check(
|
|
88
|
+
OpCode.KV_GET, namespace, _key(key), encode_get_body(opts, txn), allow_not_found=True
|
|
89
|
+
)
|
|
90
|
+
if _absent(response):
|
|
91
|
+
return None
|
|
92
|
+
return decode_get_answer(response.data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def kv_set(
|
|
96
|
+
client: "FloClient",
|
|
97
|
+
namespace: str,
|
|
98
|
+
key: str | bytes,
|
|
99
|
+
value: bytes,
|
|
100
|
+
opts: PutOptions,
|
|
101
|
+
txn: int | None = None,
|
|
102
|
+
) -> PutResult:
|
|
103
|
+
"""kv.set, inside transaction ``txn`` when given."""
|
|
104
|
+
response = await client._send_and_check(
|
|
105
|
+
OpCode.KV_PUT, namespace, _key(key), encode_set_body(value, opts, txn)
|
|
106
|
+
)
|
|
107
|
+
return decode_set_answer(response.data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def kv_delete(
|
|
111
|
+
client: "FloClient",
|
|
112
|
+
namespace: str,
|
|
113
|
+
key: str | bytes,
|
|
114
|
+
opts: DeleteOptions,
|
|
115
|
+
txn: int | None = None,
|
|
116
|
+
) -> bool | None:
|
|
117
|
+
"""kv.delete, inside transaction ``txn`` when given: whether the key existed
|
|
118
|
+
(None inside a transaction)."""
|
|
119
|
+
response = await client._send_and_check(
|
|
120
|
+
OpCode.KV_DELETE,
|
|
121
|
+
namespace,
|
|
122
|
+
_key(key),
|
|
123
|
+
encode_delete_body(opts.cas_version, opts.routing_key, txn),
|
|
124
|
+
)
|
|
125
|
+
return decode_delete_answer(response.data)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def kv_incr(
|
|
129
|
+
client: "FloClient",
|
|
130
|
+
namespace: str,
|
|
131
|
+
key: str | bytes,
|
|
132
|
+
opts: KVIncrOptions,
|
|
133
|
+
txn: int | None = None,
|
|
134
|
+
) -> tuple[int | None, int | None]:
|
|
135
|
+
"""kv.incr, inside transaction ``txn`` when given: (version, value)."""
|
|
136
|
+
response = await client._send_and_check(
|
|
137
|
+
OpCode.KV_INCR, namespace, _key(key), encode_incr_body(opts.delta, opts.routing_key, txn)
|
|
138
|
+
)
|
|
139
|
+
return decode_incr_answer(response.data)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def kv_expire(
|
|
143
|
+
client: "FloClient",
|
|
144
|
+
namespace: str,
|
|
145
|
+
key: str | bytes,
|
|
146
|
+
ttl_ms: int,
|
|
147
|
+
opts: KVExpireOptions,
|
|
148
|
+
txn: int | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""kv.expire, inside transaction ``txn`` when given."""
|
|
151
|
+
response = await client._send_and_check(
|
|
152
|
+
OpCode.KV_EXPIRE,
|
|
153
|
+
namespace,
|
|
154
|
+
_key(key),
|
|
155
|
+
encode_expire_body(ttl_ms, opts.cas_version, opts.routing_key, txn),
|
|
156
|
+
)
|
|
157
|
+
decode_empty_answer("kv.expire", response.data)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def kv_exists(
|
|
161
|
+
client: "FloClient",
|
|
162
|
+
namespace: str,
|
|
163
|
+
key: str | bytes,
|
|
164
|
+
opts: KVExistsOptions,
|
|
165
|
+
txn: int | None = None,
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""kv.exists, inside transaction ``txn`` when given."""
|
|
168
|
+
response = await client._send_and_check(
|
|
169
|
+
OpCode.KV_EXISTS, namespace, _key(key), encode_exists_body(opts.routing_key, txn)
|
|
170
|
+
)
|
|
171
|
+
return decode_exists_answer(response.data)[0]
|
|
24
172
|
|
|
25
173
|
|
|
26
174
|
class KVOperations:
|
|
@@ -33,225 +181,285 @@ class KVOperations:
|
|
|
33
181
|
self,
|
|
34
182
|
key: str | bytes,
|
|
35
183
|
options: GetOptions | None = None,
|
|
36
|
-
) ->
|
|
37
|
-
"""Get value for a key.
|
|
184
|
+
) -> GetResult | None:
|
|
185
|
+
"""Get value and version for a key.
|
|
38
186
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
options: Optional operation options (block_ms for blocking get).
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
Value bytes if found, None if not found.
|
|
187
|
+
Returns ``GetResult(value, version)``, or ``None`` if the key is not
|
|
188
|
+
found, also when a wait (``wait_ms``) ends without it.
|
|
45
189
|
|
|
46
190
|
Example:
|
|
47
|
-
|
|
48
|
-
value = await client.kv.get("user:123")
|
|
49
|
-
if value is not None:
|
|
50
|
-
print(f"Found: {value.decode()}")
|
|
51
|
-
|
|
52
|
-
# Blocking get - wait up to 5 seconds for value to appear
|
|
53
|
-
value = await client.kv.get("key", GetOptions(block_ms=5000))
|
|
191
|
+
result = await client.kv.get("user:123")
|
|
54
192
|
|
|
55
|
-
#
|
|
56
|
-
|
|
193
|
+
# Wait up to 2 seconds for the key to exist
|
|
194
|
+
result = await client.kv.get("key", GetOptions(wait_ms=2000))
|
|
57
195
|
"""
|
|
58
196
|
opts = options or GetOptions()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
key_bytes = key.encode("utf-8") if isinstance(key, str) else key
|
|
62
|
-
|
|
63
|
-
# Build TLV options
|
|
64
|
-
builder = OptionsBuilder()
|
|
65
|
-
|
|
66
|
-
if opts.block_ms is not None:
|
|
67
|
-
builder.add_u32(OptionTag.BLOCK_MS, opts.block_ms)
|
|
68
|
-
|
|
69
|
-
response = await self._client._send_and_check(
|
|
70
|
-
OpCode.KV_GET,
|
|
71
|
-
namespace,
|
|
72
|
-
key_bytes,
|
|
73
|
-
b"",
|
|
74
|
-
builder.build(),
|
|
75
|
-
allow_not_found=True,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
if response.is_not_found():
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
return response.data if response.data else None
|
|
197
|
+
return await kv_get(self._client, self._client.get_namespace(opts.namespace), key, opts)
|
|
82
198
|
|
|
83
199
|
async def put(
|
|
84
200
|
self,
|
|
85
201
|
key: str | bytes,
|
|
86
202
|
value: bytes,
|
|
87
203
|
options: PutOptions | None = None,
|
|
88
|
-
) ->
|
|
89
|
-
"""Set a key-value pair.
|
|
204
|
+
) -> PutResult:
|
|
205
|
+
"""Set a key-value pair and return the new version.
|
|
90
206
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
value: The value bytes.
|
|
94
|
-
options: Optional operation options (TTL, CAS, etc.).
|
|
207
|
+
A ``cas_version`` that isn't the key's, or a ``condition`` that doesn't
|
|
208
|
+
hold, raises a conflict with reason CAS_MISMATCH or CONDITION_FAILED.
|
|
95
209
|
|
|
96
210
|
Example:
|
|
97
|
-
|
|
98
|
-
await client.kv.put("user:123", b"
|
|
99
|
-
|
|
100
|
-
# Put with TTL
|
|
101
|
-
await client.kv.put("session:abc", b"data", PutOptions(ttl_seconds=3600))
|
|
102
|
-
|
|
103
|
-
# Put with CAS (optimistic locking)
|
|
104
|
-
await client.kv.put("counter", b"2", PutOptions(cas_version=1))
|
|
211
|
+
res = await client.kv.put("user:123", b"John Doe")
|
|
212
|
+
await client.kv.put("user:123", b"Jane", PutOptions(cas_version=res.version))
|
|
105
213
|
"""
|
|
106
214
|
opts = options or PutOptions()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
key_bytes = key.encode("utf-8") if isinstance(key, str) else key
|
|
110
|
-
|
|
111
|
-
# Build TLV options
|
|
112
|
-
builder = OptionsBuilder()
|
|
113
|
-
|
|
114
|
-
if opts.ttl_seconds is not None:
|
|
115
|
-
builder.add_u64(OptionTag.TTL_SECONDS, opts.ttl_seconds)
|
|
116
|
-
|
|
117
|
-
if opts.cas_version is not None:
|
|
118
|
-
builder.add_u64(OptionTag.CAS_VERSION, opts.cas_version)
|
|
119
|
-
|
|
120
|
-
if opts.if_not_exists:
|
|
121
|
-
builder.add_flag(OptionTag.IF_NOT_EXISTS)
|
|
122
|
-
|
|
123
|
-
if opts.if_exists:
|
|
124
|
-
builder.add_flag(OptionTag.IF_EXISTS)
|
|
125
|
-
|
|
126
|
-
await self._client._send_and_check(
|
|
127
|
-
OpCode.KV_PUT,
|
|
128
|
-
namespace,
|
|
129
|
-
key_bytes,
|
|
130
|
-
value,
|
|
131
|
-
builder.build(),
|
|
215
|
+
return await kv_set(
|
|
216
|
+
self._client, self._client.get_namespace(opts.namespace), key, value, opts
|
|
132
217
|
)
|
|
133
218
|
|
|
134
219
|
async def delete(
|
|
135
220
|
self,
|
|
136
221
|
key: str | bytes,
|
|
137
222
|
options: DeleteOptions | None = None,
|
|
138
|
-
) ->
|
|
139
|
-
"""Delete a key.
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""Delete a key and return whether it existed.
|
|
140
225
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
options: Optional operation options.
|
|
144
|
-
|
|
145
|
-
Note:
|
|
146
|
-
This operation succeeds even if the key doesn't exist.
|
|
226
|
+
Deleting a missing key is not an error. A ``cas_version`` that isn't
|
|
227
|
+
the key's raises a conflict with reason CAS_MISMATCH.
|
|
147
228
|
|
|
148
229
|
Example:
|
|
149
|
-
await client.kv.delete("user:123")
|
|
230
|
+
existed = await client.kv.delete("user:123")
|
|
231
|
+
# Only the owner releases the lock:
|
|
232
|
+
await client.kv.delete("lock:resource", DeleteOptions(cas_version=held))
|
|
150
233
|
"""
|
|
151
234
|
opts = options or DeleteOptions()
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
key_bytes = key.encode("utf-8") if isinstance(key, str) else key
|
|
155
|
-
|
|
156
|
-
# Delete succeeds for both OK and NOT_FOUND
|
|
157
|
-
await self._client._send_and_check(
|
|
158
|
-
OpCode.KV_DELETE,
|
|
159
|
-
namespace,
|
|
160
|
-
key_bytes,
|
|
161
|
-
b"",
|
|
162
|
-
allow_not_found=True,
|
|
235
|
+
existed = await kv_delete(
|
|
236
|
+
self._client, self._client.get_namespace(opts.namespace), key, opts
|
|
163
237
|
)
|
|
238
|
+
if existed is None:
|
|
239
|
+
raise IncompleteResponseError("kv.delete answer has no existed outside a transaction")
|
|
240
|
+
return existed
|
|
164
241
|
|
|
165
|
-
async def
|
|
242
|
+
async def mget(
|
|
166
243
|
self,
|
|
167
|
-
|
|
168
|
-
options:
|
|
169
|
-
) ->
|
|
170
|
-
"""
|
|
244
|
+
keys: list[str | bytes],
|
|
245
|
+
options: KVMGetOptions | None = None,
|
|
246
|
+
) -> list[MGetEntry]:
|
|
247
|
+
"""Look up many keys, one entry per key in the order asked.
|
|
248
|
+
|
|
249
|
+
Keys are split across requests so each fits one request frame, and
|
|
250
|
+
the server answers as many keys as fit one answer; this asks again
|
|
251
|
+
from where it stopped until every key is answered.
|
|
171
252
|
|
|
172
253
|
Args:
|
|
173
|
-
|
|
174
|
-
options: Optional
|
|
254
|
+
keys: Keys to fetch (max 256). Empty list returns ``[]``.
|
|
255
|
+
options: Optional namespace override, and the routing key every
|
|
256
|
+
key was written with (all are then looked up on its shard).
|
|
175
257
|
|
|
176
258
|
Returns:
|
|
177
|
-
|
|
259
|
+
List of :class:`MGetEntry`. ``found=False`` indicates a missing key.
|
|
178
260
|
|
|
179
261
|
Example:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
print(f"{entry.key}: {entry.value}")
|
|
184
|
-
|
|
185
|
-
# Paginated scan
|
|
186
|
-
result = await client.kv.scan("user:", ScanOptions(limit=100))
|
|
187
|
-
while result.has_more:
|
|
188
|
-
result = await client.kv.scan("user:", ScanOptions(cursor=result.cursor))
|
|
189
|
-
|
|
190
|
-
# Keys only (more efficient)
|
|
191
|
-
result = await client.kv.scan("user:", ScanOptions(keys_only=True))
|
|
262
|
+
for e in await client.kv.mget(["user:1", "user:2", "user:3"]):
|
|
263
|
+
if e.found:
|
|
264
|
+
print(e.key, "@v", e.version, ":", e.value)
|
|
192
265
|
"""
|
|
193
|
-
|
|
266
|
+
if not keys:
|
|
267
|
+
return []
|
|
268
|
+
opts = options or KVMGetOptions()
|
|
194
269
|
namespace = self._client.get_namespace(opts.namespace)
|
|
270
|
+
encoded = [_key(k) for k in keys]
|
|
271
|
+
names = [k if isinstance(k, str) else k.decode("utf-8", errors="replace") for k in keys]
|
|
272
|
+
encode_mget_body(encoded[:1], opts.routing_key) # refuses a bad routing key up front
|
|
273
|
+
out: list[MGetEntry] = []
|
|
274
|
+
for run in split_mget_keys(encoded):
|
|
275
|
+
done = 0
|
|
276
|
+
while done < len(run):
|
|
277
|
+
body = encode_mget_body(run[done:], opts.routing_key)
|
|
278
|
+
response = await self._client._send_and_check(OpCode.KV_MGET, namespace, b"", body)
|
|
279
|
+
entries, next_index = decode_mget_answer(
|
|
280
|
+
response.data, names[len(out) : len(out) + len(run) - done]
|
|
281
|
+
)
|
|
282
|
+
if next_index == 0:
|
|
283
|
+
raise IncompleteResponseError("kv.mget answered none of the keys asked")
|
|
284
|
+
out.extend(entries)
|
|
285
|
+
done += len(entries)
|
|
286
|
+
return out
|
|
195
287
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
builder.add_u8(OptionTag.KEYS_ONLY, 1)
|
|
288
|
+
async def scan(
|
|
289
|
+
self,
|
|
290
|
+
prefix: str | bytes = "",
|
|
291
|
+
options: ScanOptions | None = None,
|
|
292
|
+
) -> ScanResult:
|
|
293
|
+
"""List the names of keys with a prefix ("" for all), a page at a time.
|
|
203
294
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
Example:
|
|
296
|
+
result = await client.kv.scan("user:")
|
|
297
|
+
for name in result.names:
|
|
298
|
+
print(name)
|
|
208
299
|
|
|
300
|
+
# Next pages
|
|
301
|
+
while result.cursor is not None:
|
|
302
|
+
result = await client.kv.scan("user:", ScanOptions(cursor=result.cursor))
|
|
303
|
+
"""
|
|
304
|
+
opts = options or ScanOptions()
|
|
209
305
|
response = await self._client._send_and_check(
|
|
210
306
|
OpCode.KV_SCAN,
|
|
211
|
-
namespace,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
builder.build(),
|
|
307
|
+
self._client.get_namespace(opts.namespace),
|
|
308
|
+
_key(prefix),
|
|
309
|
+
encode_scan_body(opts.limit, opts.cursor),
|
|
215
310
|
)
|
|
216
|
-
|
|
217
|
-
return
|
|
311
|
+
names, cursor = decode_scan_answer(response.data)
|
|
312
|
+
return ScanResult(names=names, cursor=cursor)
|
|
218
313
|
|
|
219
314
|
async def history(
|
|
220
315
|
self,
|
|
221
316
|
key: str | bytes,
|
|
222
317
|
options: HistoryOptions | None = None,
|
|
223
318
|
) -> list[VersionEntry]:
|
|
224
|
-
"""
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
key: The key to get history for.
|
|
228
|
-
options: Optional history options (limit).
|
|
229
|
-
|
|
230
|
-
Returns:
|
|
231
|
-
List of VersionEntry with version, timestamp, and value.
|
|
319
|
+
"""A key's versions, newest first, each with when it was written; empty
|
|
320
|
+
for a missing key.
|
|
232
321
|
|
|
233
322
|
Example:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
print(f"v{entry.version}: {entry.value} at {entry.timestamp}")
|
|
323
|
+
for entry in await client.kv.history("user:123", HistoryOptions(limit=10)):
|
|
324
|
+
print(f"v{entry.version} at {entry.at_ms}: {entry.value}")
|
|
237
325
|
"""
|
|
238
326
|
opts = options or HistoryOptions()
|
|
239
|
-
|
|
327
|
+
response = await self._client._send_and_check(
|
|
328
|
+
OpCode.KV_HISTORY,
|
|
329
|
+
self._client.get_namespace(opts.namespace),
|
|
330
|
+
_key(key),
|
|
331
|
+
encode_history_body(opts.limit, opts.routing_key),
|
|
332
|
+
allow_not_found=True,
|
|
333
|
+
)
|
|
334
|
+
if _absent(response):
|
|
335
|
+
return []
|
|
336
|
+
return decode_history_answer(response.data)
|
|
240
337
|
|
|
241
|
-
|
|
338
|
+
# ── Counters, TTL, exists, JSON paths ──────────────────────────────
|
|
242
339
|
|
|
243
|
-
|
|
244
|
-
|
|
340
|
+
async def incr(
|
|
341
|
+
self,
|
|
342
|
+
key: str | bytes,
|
|
343
|
+
options: KVIncrOptions | None = None,
|
|
344
|
+
) -> IncrResult:
|
|
345
|
+
"""Atomically add ``delta`` (default +1) to the i64 counter at ``key``.
|
|
245
346
|
|
|
246
|
-
|
|
247
|
-
|
|
347
|
+
The first ``incr`` on a missing key creates it at the delta value. A
|
|
348
|
+
key that holds something else raises with reason WRONG_TYPE.
|
|
349
|
+
"""
|
|
350
|
+
opts = options or KVIncrOptions()
|
|
351
|
+
version, value = await kv_incr(
|
|
352
|
+
self._client, self._client.get_namespace(opts.namespace), key, opts
|
|
353
|
+
)
|
|
354
|
+
if version is None or value is None:
|
|
355
|
+
raise IncompleteResponseError("kv.incr answer has no value outside a transaction")
|
|
356
|
+
return IncrResult(value=value, version=version)
|
|
248
357
|
|
|
358
|
+
async def expire(
|
|
359
|
+
self,
|
|
360
|
+
key: str | bytes,
|
|
361
|
+
ttl_ms: int,
|
|
362
|
+
options: KVExpireOptions | None = None,
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Set the TTL on an existing key, in milliseconds; ``ttl_ms=0`` clears it.
|
|
365
|
+
|
|
366
|
+
With ``cas_version`` it only succeeds when the key's version is that,
|
|
367
|
+
for race-free lease renewal.
|
|
368
|
+
"""
|
|
369
|
+
opts = options or KVExpireOptions()
|
|
370
|
+
await kv_expire(self._client, self._client.get_namespace(opts.namespace), key, ttl_ms, opts)
|
|
371
|
+
|
|
372
|
+
async def exists(
|
|
373
|
+
self,
|
|
374
|
+
key: str | bytes,
|
|
375
|
+
options: KVExistsOptions | None = None,
|
|
376
|
+
) -> bool:
|
|
377
|
+
"""Return ``True`` if ``key`` is present without transferring its value."""
|
|
378
|
+
opts = options or KVExistsOptions()
|
|
379
|
+
return await kv_exists(self._client, self._client.get_namespace(opts.namespace), key, opts)
|
|
380
|
+
|
|
381
|
+
async def json_get(
|
|
382
|
+
self,
|
|
383
|
+
key: str | bytes,
|
|
384
|
+
path: str = "$",
|
|
385
|
+
options: KVJsonOptions | None = None,
|
|
386
|
+
) -> GetJsonResult[Any] | None:
|
|
387
|
+
"""Extract the value at ``path`` from the JSON document at ``key``.
|
|
388
|
+
|
|
389
|
+
Returns a :class:`GetJsonResult` whose ``value`` is the parsed JSON,
|
|
390
|
+
with the document's current version, or ``None`` if the key or path
|
|
391
|
+
is missing.
|
|
392
|
+
"""
|
|
393
|
+
opts = options or KVJsonOptions()
|
|
249
394
|
response = await self._client._send_and_check(
|
|
250
|
-
OpCode.
|
|
251
|
-
namespace,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
395
|
+
OpCode.KV_JSON_GET,
|
|
396
|
+
self._client.get_namespace(opts.namespace),
|
|
397
|
+
_key(key),
|
|
398
|
+
encode_json_get_body(path, opts.routing_key),
|
|
399
|
+
allow_not_found=True,
|
|
400
|
+
)
|
|
401
|
+
if _absent(response):
|
|
402
|
+
return None
|
|
403
|
+
version, document = decode_json_get_answer(response.data)
|
|
404
|
+
return GetJsonResult(value=json.loads(document.decode("utf-8")), version=version)
|
|
405
|
+
|
|
406
|
+
async def json_set(
|
|
407
|
+
self,
|
|
408
|
+
key: str | bytes,
|
|
409
|
+
path: str,
|
|
410
|
+
json_value: bytes,
|
|
411
|
+
options: KVJsonOptions | None = None,
|
|
412
|
+
) -> PutResult:
|
|
413
|
+
"""Set the JSON value at ``path`` ("$" when empty) inside the document at ``key``.
|
|
414
|
+
|
|
415
|
+
Path ``"$"`` replaces the whole document (and creates the key if missing).
|
|
416
|
+
Sub-paths require the key to already exist. Returns the new version.
|
|
417
|
+
"""
|
|
418
|
+
opts = options or KVJsonOptions()
|
|
419
|
+
response = await self._client._send_and_check(
|
|
420
|
+
OpCode.KV_JSON_SET,
|
|
421
|
+
self._client.get_namespace(opts.namespace),
|
|
422
|
+
_key(key),
|
|
423
|
+
encode_json_set_body(path, json_value, opts.routing_key),
|
|
255
424
|
)
|
|
425
|
+
return PutResult(version=decode_json_set_answer(response.data))
|
|
426
|
+
|
|
427
|
+
async def json_del(
|
|
428
|
+
self,
|
|
429
|
+
key: str | bytes,
|
|
430
|
+
path: str = "$",
|
|
431
|
+
options: KVJsonOptions | None = None,
|
|
432
|
+
) -> PutResult | None:
|
|
433
|
+
"""Remove the value at ``path`` from the JSON document at ``key``.
|
|
434
|
+
|
|
435
|
+
Returns the document's new version, or ``None`` for ``"$"``, which
|
|
436
|
+
deletes the key.
|
|
437
|
+
"""
|
|
438
|
+
opts = options or KVJsonOptions()
|
|
439
|
+
response = await self._client._send_and_check(
|
|
440
|
+
OpCode.KV_JSON_DEL,
|
|
441
|
+
self._client.get_namespace(opts.namespace),
|
|
442
|
+
_key(key),
|
|
443
|
+
encode_json_del_body(path, opts.routing_key),
|
|
444
|
+
)
|
|
445
|
+
version = decode_json_del_answer(response.data)
|
|
446
|
+
return None if version is None else PutResult(version=version)
|
|
447
|
+
|
|
448
|
+
async def begin(self, routing_key: str) -> "Transaction":
|
|
449
|
+
"""Open a per-shard KV transaction pinned to ``routing_key``'s partition.
|
|
450
|
+
|
|
451
|
+
Every op inside the transaction is routed by ``routing_key``; the
|
|
452
|
+
server refuses one routed otherwise with reason WRONG_ROUTING_KEY.
|
|
453
|
+
|
|
454
|
+
Returns a :class:`Transaction` handle. Use as an async context manager
|
|
455
|
+
for automatic rollback on exception::
|
|
456
|
+
|
|
457
|
+
async with await client.kv.begin("user:123") as txn:
|
|
458
|
+
await txn.put("user:123:name", b"Jane")
|
|
459
|
+
await txn.incr("user:123:visits", 1)
|
|
460
|
+
await txn.commit()
|
|
461
|
+
"""
|
|
462
|
+
from .kv_txn import begin as _begin
|
|
256
463
|
|
|
257
|
-
|
|
464
|
+
namespace = self._client.get_namespace(None)
|
|
465
|
+
return await _begin(self._client, namespace, routing_key)
|