oxia 0.1.0__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.
- oxia/__init__.py +38 -0
- oxia/client.py +300 -0
- oxia/defs.py +165 -0
- oxia/ex.py +28 -0
- oxia/internal/__init__.py +14 -0
- oxia/internal/backoff.py +28 -0
- oxia/internal/compare.py +53 -0
- oxia/internal/connection_pool.py +36 -0
- oxia/internal/notifications.py +115 -0
- oxia/internal/oxia_container.py +28 -0
- oxia/internal/proto/__init__.py +0 -0
- oxia/internal/proto/client.proto +508 -0
- oxia/internal/proto/io/__init__.py +0 -0
- oxia/internal/proto/io/streamnative/__init__.py +0 -0
- oxia/internal/proto/io/streamnative/oxia/__init__.py +0 -0
- oxia/internal/proto/io/streamnative/oxia/proto/__init__.py +1135 -0
- oxia/internal/proto/message_pool.py +3 -0
- oxia/internal/proto/py.typed +0 -0
- oxia/internal/sequence_updates.py +75 -0
- oxia/internal/service_discovery.py +124 -0
- oxia/internal/sessions.py +73 -0
- oxia-0.1.0.dist-info/METADATA +9 -0
- oxia-0.1.0.dist-info/RECORD +24 -0
- oxia-0.1.0.dist-info/WHEEL +4 -0
oxia/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Oxia Python Client SDK
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from oxia.client import (
|
|
20
|
+
Client,
|
|
21
|
+
)
|
|
22
|
+
from oxia.defs import (
|
|
23
|
+
ComparisonType,
|
|
24
|
+
NotificationType,
|
|
25
|
+
Notification,
|
|
26
|
+
Version,
|
|
27
|
+
EXPECTED_RECORD_DOES_NOT_EXIST,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'ex',
|
|
32
|
+
'ComparisonType',
|
|
33
|
+
'Client',
|
|
34
|
+
'Version',
|
|
35
|
+
'EXPECTED_RECORD_DOES_NOT_EXIST',
|
|
36
|
+
'NotificationType',
|
|
37
|
+
'Notification',
|
|
38
|
+
]
|
oxia/client.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import functools
|
|
15
|
+
import heapq
|
|
16
|
+
from typing import Iterator
|
|
17
|
+
|
|
18
|
+
from oxia.internal.compare import compare_tuple_with_slash, compare_with_slash
|
|
19
|
+
from oxia.internal.connection_pool import ConnectionPool
|
|
20
|
+
from oxia.internal.notifications import Notifications
|
|
21
|
+
from oxia.internal.sequence_updates import SequenceUpdatesImpl
|
|
22
|
+
from oxia.internal.sessions import SessionManager
|
|
23
|
+
from oxia.internal.service_discovery import ServiceDiscovery
|
|
24
|
+
from oxia.internal.proto.io.streamnative.oxia import proto as pb
|
|
25
|
+
import oxia.ex
|
|
26
|
+
|
|
27
|
+
import datetime
|
|
28
|
+
|
|
29
|
+
def _check_status(status: pb.Status):
|
|
30
|
+
if status == pb.Status.OK:
|
|
31
|
+
pass
|
|
32
|
+
elif status == pb.Status.KEY_NOT_FOUND:
|
|
33
|
+
raise oxia.ex.KeyNotFound()
|
|
34
|
+
elif status == pb.Status.UNEXPECTED_VERSION_ID:
|
|
35
|
+
raise oxia.ex.UnexpectedVersionId()
|
|
36
|
+
elif status == pb.Status.SESSION_DOES_NOT_EXIST:
|
|
37
|
+
raise oxia.ex.SessionNotFound()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_version(pbv : pb.Version):
|
|
41
|
+
v = oxia.defs.Version()
|
|
42
|
+
v._version_id = pbv.version_id
|
|
43
|
+
v._modifications_count = pbv.modifications_count
|
|
44
|
+
v._created_timestamp = datetime.datetime.fromtimestamp(pbv.created_timestamp/1000.0)
|
|
45
|
+
v._modified_timestamp = datetime.datetime.fromtimestamp(pbv.modified_timestamp/1000.0)
|
|
46
|
+
v._session_id = pbv.session_id
|
|
47
|
+
v._client_identity = pbv.client_identity
|
|
48
|
+
return v
|
|
49
|
+
|
|
50
|
+
class Client:
|
|
51
|
+
def __init__(self, service_address: str,
|
|
52
|
+
namespace: str = "default",
|
|
53
|
+
session_timeout_ms: int = 30_000,
|
|
54
|
+
client_identifier: str = None,
|
|
55
|
+
):
|
|
56
|
+
self._closed = False
|
|
57
|
+
self._connections = ConnectionPool()
|
|
58
|
+
self._service_discovery = ServiceDiscovery(service_address, self._connections, namespace)
|
|
59
|
+
self._session_manager = SessionManager(self._service_discovery, session_timeout_ms, client_identifier)
|
|
60
|
+
|
|
61
|
+
def put(self, key: str, value: object,
|
|
62
|
+
partition_key: str = None,
|
|
63
|
+
expected_version_id: int = None,
|
|
64
|
+
ephemeral: bool = False,
|
|
65
|
+
sequence_keys_deltas: list[int] = None,
|
|
66
|
+
secondary_indexes: dict[str, str] = None,
|
|
67
|
+
) -> (str, oxia.defs.Version):
|
|
68
|
+
"""
|
|
69
|
+
Put Associates a value with a key
|
|
70
|
+
|
|
71
|
+
There are few options that can be passed to the Put operation:
|
|
72
|
+
- The Put operation can be made conditional on that the record hasn't changed from
|
|
73
|
+
a specific existing version by passing the [ExpectedVersionId] option.
|
|
74
|
+
- Client can assert that the record does not exist by passing [ExpectedRecordNotExists]
|
|
75
|
+
- Client can create an ephemeral record with [Ephemeral]
|
|
76
|
+
|
|
77
|
+
Returns the actual key of the inserted record
|
|
78
|
+
Returns a [Version] object that contains information about the newly updated record
|
|
79
|
+
Throw [ErrorUnexpectedVersionId] if the expected version id does not match the
|
|
80
|
+
current version id of the record
|
|
81
|
+
|
|
82
|
+
:param key:
|
|
83
|
+
:param value:
|
|
84
|
+
:param partition_key:
|
|
85
|
+
:param expected_version_id:
|
|
86
|
+
:param ephemeral:
|
|
87
|
+
:return:
|
|
88
|
+
"""
|
|
89
|
+
shard, stub = self._service_discovery.get_leader(key, partition_key)
|
|
90
|
+
|
|
91
|
+
if sequence_keys_deltas:
|
|
92
|
+
if not partition_key:
|
|
93
|
+
raise oxia.ex.InvalidOptions("sequence_keys_deltas can only be used with partition_key")
|
|
94
|
+
if expected_version_id is not None:
|
|
95
|
+
raise oxia.ex.InvalidOptions("sequence_keys_deltas cannot be used with expected_version_id")
|
|
96
|
+
|
|
97
|
+
if type(value) is str:
|
|
98
|
+
value = bytes(str(value), encoding='utf-8')
|
|
99
|
+
|
|
100
|
+
pr = pb.PutRequest(key=key, value=value,
|
|
101
|
+
partition_key=partition_key,
|
|
102
|
+
expected_version_id=expected_version_id,
|
|
103
|
+
sequence_key_delta=sequence_keys_deltas,
|
|
104
|
+
)
|
|
105
|
+
if ephemeral:
|
|
106
|
+
session = self._session_manager.get_session(shard)
|
|
107
|
+
pr.session_id = session.session_id()
|
|
108
|
+
pr.client_identity = session.client_identifier()
|
|
109
|
+
|
|
110
|
+
if secondary_indexes:
|
|
111
|
+
indexes = []
|
|
112
|
+
for k, v in secondary_indexes.items():
|
|
113
|
+
indexes.append(pb.SecondaryIndex(k, v))
|
|
114
|
+
pr.secondary_indexes = indexes
|
|
115
|
+
|
|
116
|
+
res = stub.write(pb.WriteRequest(shard=shard, puts=[pr]))
|
|
117
|
+
|
|
118
|
+
put_res = res.puts[0] # We only have 1 request
|
|
119
|
+
_check_status(put_res.status)
|
|
120
|
+
|
|
121
|
+
if put_res.key:
|
|
122
|
+
key = put_res.key
|
|
123
|
+
return key, _get_version(put_res.version)
|
|
124
|
+
|
|
125
|
+
def delete(self, key: str,
|
|
126
|
+
partition_key: str = None,
|
|
127
|
+
expected_version_id: int = None, ) -> bool:
|
|
128
|
+
# // Delete removes the key and its associated value from the data store.
|
|
129
|
+
# //
|
|
130
|
+
# // The Delete operation can be made conditional on that the record hasn't changed from
|
|
131
|
+
# // a specific existing version by passing the [ExpectedVersionId] option.
|
|
132
|
+
# // Returns [ErrorUnexpectedVersionId] if the expected version id does not match the
|
|
133
|
+
# // current version id of the record
|
|
134
|
+
shard, stub = self._service_discovery.get_leader(key, partition_key)
|
|
135
|
+
|
|
136
|
+
dr = pb.DeleteRequest(key=key,
|
|
137
|
+
expected_version_id=expected_version_id)
|
|
138
|
+
res = stub.write(pb.WriteRequest(shard=shard, deletes=[dr]))
|
|
139
|
+
|
|
140
|
+
status = res.deletes[0].status # We only have 1 request
|
|
141
|
+
if status == pb.Status.KEY_NOT_FOUND:
|
|
142
|
+
return False
|
|
143
|
+
else:
|
|
144
|
+
_check_status(status)
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
def delete_range(self, min_key_inclusive: str, max_key_exclusive: str, partition_key: str = None):
|
|
148
|
+
# // DeleteRange deletes any records with keys within the specified range.
|
|
149
|
+
# // Note: Oxia uses a custom sorting order that treats `/` characters in special way.
|
|
150
|
+
# // Refer to this documentation for the specifics:
|
|
151
|
+
# // https://github.com/streamnative/oxia/blob/main/docs/oxia-key-sorting.md
|
|
152
|
+
if partition_key is None:
|
|
153
|
+
for shard, stub in self._service_discovery.get_all_shards():
|
|
154
|
+
self._delete_range_single_shard(min_key_inclusive, max_key_exclusive, shard, stub)
|
|
155
|
+
else:
|
|
156
|
+
shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
|
|
157
|
+
self._delete_range_single_shard(min_key_inclusive, max_key_exclusive, shard, stub)
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _delete_range_single_shard(min_key_inclusive: str, max_key_exclusive: str, shard: int, stub):
|
|
161
|
+
dr = pb.DeleteRangeRequest(start_inclusive=min_key_inclusive,
|
|
162
|
+
end_exclusive=max_key_exclusive)
|
|
163
|
+
res = stub.write(pb.WriteRequest(shard=shard, delete_ranges=[dr]))
|
|
164
|
+
|
|
165
|
+
status = res.delete_ranges[0].status # We only have 1 request
|
|
166
|
+
_check_status(status)
|
|
167
|
+
|
|
168
|
+
def get(self, key: str,
|
|
169
|
+
partition_key: str = None,
|
|
170
|
+
comparison_type: oxia.defs.ComparisonType = oxia.defs.ComparisonType.EQUAL,
|
|
171
|
+
include_value: bool = True,
|
|
172
|
+
use_index: str = None,
|
|
173
|
+
) -> (str, str, oxia.defs.Version):
|
|
174
|
+
# // Get returns the value associated with the specified key.
|
|
175
|
+
# // In addition to the value, a version object is also returned, with information
|
|
176
|
+
# // about the record state.
|
|
177
|
+
# // Returns ErrorKeyNotFound if the record does not exist
|
|
178
|
+
if partition_key is None and \
|
|
179
|
+
(comparison_type != oxia.defs.ComparisonType.EQUAL
|
|
180
|
+
or use_index is not None):
|
|
181
|
+
|
|
182
|
+
results = []
|
|
183
|
+
for shard, stub in self._service_discovery.get_all_shards():
|
|
184
|
+
try:
|
|
185
|
+
k, val, version = self._get_single_shard(shard, stub, key, comparison_type, include_value, use_index)
|
|
186
|
+
results.append((k, val, version))
|
|
187
|
+
except oxia.ex.KeyNotFound:
|
|
188
|
+
pass
|
|
189
|
+
if not results: raise oxia.ex.KeyNotFound
|
|
190
|
+
results.sort(key=functools.cmp_to_key(compare_tuple_with_slash))
|
|
191
|
+
|
|
192
|
+
if comparison_type == oxia.defs.ComparisonType.EQUAL or \
|
|
193
|
+
comparison_type == oxia.defs.ComparisonType.CEILING or \
|
|
194
|
+
comparison_type == oxia.defs.ComparisonType.HIGHER:
|
|
195
|
+
return results[0]
|
|
196
|
+
elif comparison_type == oxia.defs.ComparisonType.FLOOR or \
|
|
197
|
+
comparison_type == oxia.defs.ComparisonType.LOWER:
|
|
198
|
+
return results[-1]
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
# Single shard get operation
|
|
202
|
+
shard, stub = self._service_discovery.get_leader(key, partition_key)
|
|
203
|
+
return self._get_single_shard(shard, stub, key, comparison_type, include_value, use_index)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _get_single_shard(shard: int, stub,
|
|
207
|
+
key: str,
|
|
208
|
+
comparison_type: oxia.defs.ComparisonType,
|
|
209
|
+
include_value: bool,
|
|
210
|
+
use_index: str) -> (str, str, oxia.defs.Version):
|
|
211
|
+
gr = pb.GetRequest(key=key,
|
|
212
|
+
comparison_type=comparison_type,
|
|
213
|
+
include_value=include_value,
|
|
214
|
+
secondary_index_name=use_index)
|
|
215
|
+
res = next(stub.read(pb.ReadRequest(shard=shard, gets=[gr])))
|
|
216
|
+
get_res = res.gets[0] # We only have 1 request
|
|
217
|
+
_check_status(get_res.status)
|
|
218
|
+
|
|
219
|
+
res_key = get_res.key if get_res.key is not None else key
|
|
220
|
+
return res_key, get_res.value, _get_version(get_res.version)
|
|
221
|
+
|
|
222
|
+
def list(self, min_key_inclusive: str, max_key_exclusive: str,
|
|
223
|
+
partition_key: str = None,
|
|
224
|
+
use_index: str = None,
|
|
225
|
+
):
|
|
226
|
+
# // List any existing keys within the specified range.
|
|
227
|
+
# // Note: Oxia uses a custom sorting order that treats `/` characters in special way.
|
|
228
|
+
# // Refer to this documentation for the specifics:
|
|
229
|
+
# // https://github.com/streamnative/oxia/blob/main/docs/oxia-key-sorting.md
|
|
230
|
+
if partition_key is None:
|
|
231
|
+
all_res = []
|
|
232
|
+
for shard, stub in self._service_discovery.get_all_shards():
|
|
233
|
+
all_res.extend(self._list_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index))
|
|
234
|
+
all_res.sort(key=functools.cmp_to_key(compare_with_slash))
|
|
235
|
+
return all_res
|
|
236
|
+
else:
|
|
237
|
+
shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
|
|
238
|
+
return self._list_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _list_single_shard(shard, stub, min_key_inclusive: str, max_key_exclusive: str, use_index: str) -> list:
|
|
242
|
+
res = stub.list(pb.ListRequest(shard=shard,
|
|
243
|
+
start_inclusive=min_key_inclusive,
|
|
244
|
+
end_exclusive=max_key_exclusive,
|
|
245
|
+
secondary_index_name=use_index))
|
|
246
|
+
|
|
247
|
+
keys = []
|
|
248
|
+
for i in res:
|
|
249
|
+
keys.extend(i.keys)
|
|
250
|
+
return keys
|
|
251
|
+
|
|
252
|
+
def range_scan(self,
|
|
253
|
+
min_key_inclusive: str,
|
|
254
|
+
max_key_exclusive: str,
|
|
255
|
+
partition_key: str = None,
|
|
256
|
+
use_index: str = None,
|
|
257
|
+
) -> Iterator[tuple[str, str, oxia.defs.Version]]:
|
|
258
|
+
# // RangeScan perform a scan for existing records with any keys within the specified range.
|
|
259
|
+
# // Ordering in results channel is respected only if a [PartitionKey] option is passed (and the keys were
|
|
260
|
+
# // inserted with that partition key).
|
|
261
|
+
if partition_key is None:
|
|
262
|
+
its = []
|
|
263
|
+
for shard, stub in self._service_discovery.get_all_shards():
|
|
264
|
+
its.append(self._range_scan_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index))
|
|
265
|
+
return heapq.merge(*its, key=functools.cmp_to_key(compare_tuple_with_slash))
|
|
266
|
+
else:
|
|
267
|
+
shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
|
|
268
|
+
return self._range_scan_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index)
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _range_scan_single_shard(shard, stub, min_key_inclusive: str, max_key_exclusive: str, use_index: str) \
|
|
272
|
+
-> Iterator[tuple[str, str, oxia.defs.Version]]:
|
|
273
|
+
it = stub.range_scan(pb.RangeScanRequest(shard=shard,
|
|
274
|
+
start_inclusive=min_key_inclusive,
|
|
275
|
+
end_exclusive=max_key_exclusive,
|
|
276
|
+
secondary_index_name=use_index))
|
|
277
|
+
|
|
278
|
+
for res in it:
|
|
279
|
+
for x in res.records:
|
|
280
|
+
yield x.key, x.value, _get_version(x.version)
|
|
281
|
+
|
|
282
|
+
def get_sequence_updates(self, prefix_key: str, partition_key: str = None) -> oxia.defs.SequenceUpdates:
|
|
283
|
+
# // GetSequenceUpdates allows to subscribe to the updates happening on a sequential key
|
|
284
|
+
# // The channel will report the current latest sequence for a given key.
|
|
285
|
+
# // Multiple updates can be collapsed into one single event with the
|
|
286
|
+
# // highest sequence.
|
|
287
|
+
if partition_key is None:
|
|
288
|
+
raise oxia.ex.InvalidOptions("get_sequence_updates requires a partition_key")
|
|
289
|
+
return SequenceUpdatesImpl(self._service_discovery, prefix_key, partition_key, lambda : self._closed)
|
|
290
|
+
|
|
291
|
+
def get_notifications(self):
|
|
292
|
+
"""GetNotifications creates a new subscription to receive the notifications
|
|
293
|
+
from Oxia for any change that is applied to the database"""
|
|
294
|
+
return Notifications(self._service_discovery)
|
|
295
|
+
|
|
296
|
+
def close(self):
|
|
297
|
+
self._closed = True
|
|
298
|
+
self._session_manager.close()
|
|
299
|
+
self._connections.close()
|
|
300
|
+
self._service_discovery.close()
|
oxia/defs.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from enum import Enum, IntEnum
|
|
16
|
+
from abc import ABC
|
|
17
|
+
from oxia.internal.proto.io.streamnative.oxia import proto as pb
|
|
18
|
+
import datetime
|
|
19
|
+
from typing import Iterator
|
|
20
|
+
|
|
21
|
+
class NotificationType(Enum):
|
|
22
|
+
"""NotificationType represents the type of the notification event."""
|
|
23
|
+
|
|
24
|
+
"""KeyCreated A record that didn't exist was created."""
|
|
25
|
+
KEY_CREATED = 0
|
|
26
|
+
|
|
27
|
+
"""KeyModified An existing record was modified."""
|
|
28
|
+
KEY_MODIFIED = 1
|
|
29
|
+
|
|
30
|
+
"""KeyDeleted A record was deleted."""
|
|
31
|
+
KEY_DELETED = 2
|
|
32
|
+
|
|
33
|
+
"""KeyRangeDeleted A range of keys was deleted."""
|
|
34
|
+
KEY_RANGE_DELETED = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Notification:
|
|
38
|
+
"""Notification represents one change in the Oxia database."""
|
|
39
|
+
|
|
40
|
+
def notification_type(self) -> NotificationType:
|
|
41
|
+
"""The type of the modification"""
|
|
42
|
+
return self._type
|
|
43
|
+
|
|
44
|
+
def key(self) -> str:
|
|
45
|
+
"""The Key of the record to which the notification is referring"""
|
|
46
|
+
return self._key
|
|
47
|
+
|
|
48
|
+
def version_id(self) -> int:
|
|
49
|
+
"""The current VersionId of the record, or -1 for a KeyDeleted event"""
|
|
50
|
+
return self._version_id
|
|
51
|
+
|
|
52
|
+
def key_range_end(self) -> str:
|
|
53
|
+
"""In case of a KeyRangeRangeDeleted notification, this would represent
|
|
54
|
+
the end (excluded) of the range of keys"""
|
|
55
|
+
return self._key_range_end
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return f"Notification(key: {self.key()}, type: {self.notification_type()}, version_id: {self.version_id()}, key_range_end: {self.key_range_end()})"
|
|
59
|
+
|
|
60
|
+
class ComparisonType(IntEnum):
|
|
61
|
+
"""ComparisonType is an enumeration of the possible comparison types for the `get()` operation."""
|
|
62
|
+
|
|
63
|
+
"""Equal sets the Get() operation to compare the stored key for equality."""
|
|
64
|
+
EQUAL = pb.KeyComparisonType.EQUAL
|
|
65
|
+
|
|
66
|
+
"""Floor option will make the get operation to search for the record whose key is the
|
|
67
|
+
highest key <= to the supplied key."""
|
|
68
|
+
FLOOR = pb.KeyComparisonType.FLOOR
|
|
69
|
+
|
|
70
|
+
"""Ceiling option will make the get operation to search for the record whose key is the
|
|
71
|
+
lowest key >= to the supplied key."""
|
|
72
|
+
CEILING = pb.KeyComparisonType.CEILING
|
|
73
|
+
|
|
74
|
+
"""Lower option will make the get operation to search for the record whose key is strictly < to
|
|
75
|
+
the supplied key."""
|
|
76
|
+
LOWER = pb.KeyComparisonType.LOWER
|
|
77
|
+
|
|
78
|
+
"""Higher option will make the get operation to search for the record whose key is
|
|
79
|
+
strictly > to the supplied key."""
|
|
80
|
+
HIGHER = pb.KeyComparisonType.HIGHER
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Version:
|
|
84
|
+
"""
|
|
85
|
+
Version includes some information regarding the state of a record.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def version_id(self) -> int:
|
|
89
|
+
"""
|
|
90
|
+
Retrieve the version ID.
|
|
91
|
+
|
|
92
|
+
This method returns the version ID, which is an integer value representing
|
|
93
|
+
the current version. It does not accept any parameters and simply outputs
|
|
94
|
+
the version ID as an integer.
|
|
95
|
+
|
|
96
|
+
:return: The version ID.
|
|
97
|
+
:rtype: int
|
|
98
|
+
"""
|
|
99
|
+
return self._version_id
|
|
100
|
+
|
|
101
|
+
def created_timestamp(self) -> datetime.datetime:
|
|
102
|
+
"""
|
|
103
|
+
The time when the record was last created
|
|
104
|
+
(If the record gets deleted and recreated, it will have a new CreatedTimestamp value)
|
|
105
|
+
"""
|
|
106
|
+
return self._created_timestamp
|
|
107
|
+
|
|
108
|
+
def modified_timestamp(self) -> datetime.datetime:
|
|
109
|
+
"""
|
|
110
|
+
Get the time when the record was last modified.
|
|
111
|
+
|
|
112
|
+
:return: The last modification timestamp.
|
|
113
|
+
:rtype: int
|
|
114
|
+
"""
|
|
115
|
+
return self._modified_timestamp
|
|
116
|
+
|
|
117
|
+
def modifications_count(self) -> int:
|
|
118
|
+
"""
|
|
119
|
+
Get the number of modifications to the record since it was last created.
|
|
120
|
+
If the record gets deleted and recreated, the ModificationsCount will restart at 0.
|
|
121
|
+
|
|
122
|
+
:return: The number of modifications.
|
|
123
|
+
:rtype: int
|
|
124
|
+
"""
|
|
125
|
+
return self._modifications_count
|
|
126
|
+
|
|
127
|
+
def is_ephemeral(self) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Check if the record is ephemeral.
|
|
130
|
+
|
|
131
|
+
:return: True if the record is ephemeral, False otherwise.
|
|
132
|
+
:rtype: bool
|
|
133
|
+
"""
|
|
134
|
+
return self._session_id is not None
|
|
135
|
+
|
|
136
|
+
def session_id(self) -> int:
|
|
137
|
+
"""
|
|
138
|
+
Get the session identifier for ephemeral records.
|
|
139
|
+
For ephemeral records, this is the identifier of the session to which this record lifecycle
|
|
140
|
+
is attached to. Non-ephemeral records will always report 0.
|
|
141
|
+
|
|
142
|
+
:return: The session ID.
|
|
143
|
+
:rtype: int
|
|
144
|
+
"""
|
|
145
|
+
return self._session_id
|
|
146
|
+
|
|
147
|
+
def client_identity(self) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Get the client identity for ephemeral records.
|
|
150
|
+
For ephemeral records, this is the unique identity of the Oxia client that did last modify it.
|
|
151
|
+
It will be empty for all non-ephemeral records.
|
|
152
|
+
|
|
153
|
+
:return: The client identity.
|
|
154
|
+
:rtype: str
|
|
155
|
+
"""
|
|
156
|
+
return self._client_identity
|
|
157
|
+
|
|
158
|
+
def __str__(self):
|
|
159
|
+
return f"Version(version_id: {self.version_id()}, session_id: {self.session_id()}, modifications_count: {self.modifications_count()}, created_timestamp: {self.created_timestamp()}, modified_timestamp: {self.modified_timestamp()}, client_identity: {self.client_identity()})"
|
|
160
|
+
|
|
161
|
+
EXPECTED_RECORD_DOES_NOT_EXIST = -1
|
|
162
|
+
|
|
163
|
+
class SequenceUpdates(Iterator[str], ABC):
|
|
164
|
+
def close(self):
|
|
165
|
+
pass
|
oxia/ex.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
class OxiaException(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class InvalidOptions(OxiaException):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class KeyNotFound(OxiaException):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
class UnexpectedVersionId(OxiaException):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class SessionNotFound(OxiaException):
|
|
28
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
oxia/internal/backoff.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
class Backoff:
|
|
18
|
+
def __init__(self, start_delay=0.1):
|
|
19
|
+
self.start_delay = start_delay
|
|
20
|
+
self.delay = start_delay
|
|
21
|
+
self.max_delay = 60.0
|
|
22
|
+
|
|
23
|
+
def reset(self):
|
|
24
|
+
self.delay = self.start_delay
|
|
25
|
+
|
|
26
|
+
def wait_next(self):
|
|
27
|
+
time.sleep(self.delay)
|
|
28
|
+
self.delay = min(self.delay * 2, self.max_delay)
|
oxia/internal/compare.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
def _cmp(a: str, b: str) -> int:
|
|
16
|
+
if a < b:
|
|
17
|
+
return -1
|
|
18
|
+
elif a > b:
|
|
19
|
+
return +1
|
|
20
|
+
else:
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
def compare_tuple_with_slash(a, b) -> int:
|
|
24
|
+
return compare_with_slash(a[0], b[0])
|
|
25
|
+
|
|
26
|
+
def compare_with_slash(a : str, b : str) -> int:
|
|
27
|
+
while a and b:
|
|
28
|
+
idx_a = a.find('/')
|
|
29
|
+
idx_b = b.find('/')
|
|
30
|
+
if idx_a < 0 and idx_b < 0:
|
|
31
|
+
return _cmp(a, b)
|
|
32
|
+
elif idx_a < 0:
|
|
33
|
+
return -1
|
|
34
|
+
elif idx_b < 0:
|
|
35
|
+
return +1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# At this point, both slices have '/'
|
|
39
|
+
span_a = a[:idx_a]
|
|
40
|
+
span_b = b[:idx_b]
|
|
41
|
+
span_cmp = _cmp(span_a, span_b)
|
|
42
|
+
if span_cmp != 0:
|
|
43
|
+
return span_cmp
|
|
44
|
+
|
|
45
|
+
a = a[idx_a+1:]
|
|
46
|
+
b = b[idx_b+1:]
|
|
47
|
+
|
|
48
|
+
if len(a) < len(b):
|
|
49
|
+
return -1
|
|
50
|
+
elif len(a) > 0:
|
|
51
|
+
return +1
|
|
52
|
+
else:
|
|
53
|
+
return 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright 2025 StreamNative, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import grpc
|
|
16
|
+
from oxia.internal.proto.io.streamnative.oxia.proto import OxiaClientStub
|
|
17
|
+
|
|
18
|
+
class ConnectionPool:
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.connections = {}
|
|
22
|
+
|
|
23
|
+
def get(self, address) -> OxiaClientStub:
|
|
24
|
+
x = self.connections.get(address)
|
|
25
|
+
if x is None:
|
|
26
|
+
channel = grpc.insecure_channel(address)
|
|
27
|
+
stub = OxiaClientStub(channel)
|
|
28
|
+
x = (channel, stub)
|
|
29
|
+
self.connections[address] = x
|
|
30
|
+
|
|
31
|
+
return x[1]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def close(self):
|
|
35
|
+
for c in self.connections.values():
|
|
36
|
+
c[0].close()
|