oxia 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oxia-0.1.0/.github/workflows/pypi-release.yaml +44 -0
- oxia-0.1.0/.github/workflows/run-tests.yaml +43 -0
- oxia-0.1.0/.gitignore +7 -0
- oxia-0.1.0/.python-version +1 -0
- oxia-0.1.0/PKG-INFO +9 -0
- oxia-0.1.0/gen-proto.sh +19 -0
- oxia-0.1.0/pydoctor.ini +13 -0
- oxia-0.1.0/pyproject.toml +48 -0
- oxia-0.1.0/src/oxia/__init__.py +38 -0
- oxia-0.1.0/src/oxia/client.py +300 -0
- oxia-0.1.0/src/oxia/defs.py +165 -0
- oxia-0.1.0/src/oxia/ex.py +28 -0
- oxia-0.1.0/src/oxia/internal/__init__.py +14 -0
- oxia-0.1.0/src/oxia/internal/backoff.py +28 -0
- oxia-0.1.0/src/oxia/internal/compare.py +53 -0
- oxia-0.1.0/src/oxia/internal/connection_pool.py +36 -0
- oxia-0.1.0/src/oxia/internal/notifications.py +115 -0
- oxia-0.1.0/src/oxia/internal/oxia_container.py +28 -0
- oxia-0.1.0/src/oxia/internal/proto/__init__.py +0 -0
- oxia-0.1.0/src/oxia/internal/proto/client.proto +508 -0
- oxia-0.1.0/src/oxia/internal/proto/io/__init__.py +0 -0
- oxia-0.1.0/src/oxia/internal/proto/io/streamnative/__init__.py +0 -0
- oxia-0.1.0/src/oxia/internal/proto/io/streamnative/oxia/__init__.py +0 -0
- oxia-0.1.0/src/oxia/internal/proto/io/streamnative/oxia/proto/__init__.py +1135 -0
- oxia-0.1.0/src/oxia/internal/proto/message_pool.py +3 -0
- oxia-0.1.0/src/oxia/internal/proto/py.typed +0 -0
- oxia-0.1.0/src/oxia/internal/sequence_updates.py +75 -0
- oxia-0.1.0/src/oxia/internal/service_discovery.py +124 -0
- oxia-0.1.0/src/oxia/internal/sessions.py +73 -0
- oxia-0.1.0/tests/client_test.py +740 -0
- oxia-0.1.0/tests/compare_test.py +46 -0
- oxia-0.1.0/uv.lock +746 -0
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
name: Publish to PyPI
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags:
|
|
20
|
+
- v*
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build_and_publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi # Optional: use a GitHub environment for added security
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # Required for OIDC authentication
|
|
28
|
+
contents: read # Required to checkout the repository
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- name: Checkout repository
|
|
32
|
+
uses: actions/checkout@v5
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@v6
|
|
36
|
+
|
|
37
|
+
- name: Install Python
|
|
38
|
+
run: uv python install 3.13
|
|
39
|
+
|
|
40
|
+
- name: Build distribution packages
|
|
41
|
+
run: uv build
|
|
42
|
+
|
|
43
|
+
- name: Publish package to PyPI
|
|
44
|
+
run: uv publish
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
name: Run tests
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
pull_request:
|
|
19
|
+
branches:
|
|
20
|
+
- main
|
|
21
|
+
push:
|
|
22
|
+
branches:
|
|
23
|
+
- main
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
build_and_test:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout repository
|
|
31
|
+
uses: actions/checkout@v5
|
|
32
|
+
|
|
33
|
+
- name: Install uv
|
|
34
|
+
uses: astral-sh/setup-uv@v6
|
|
35
|
+
|
|
36
|
+
- name: Install Python
|
|
37
|
+
run: uv python install 3.13
|
|
38
|
+
|
|
39
|
+
- name: Build distribution packages
|
|
40
|
+
run: uv build
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: uv run pytest
|
oxia-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
oxia-0.1.0/PKG-INFO
ADDED
oxia-0.1.0/gen-proto.sh
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Copyright 2025 StreamNative, Inc.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
source .venv/bin/activate
|
|
18
|
+
|
|
19
|
+
python -m grpc.tools.protoc -I. --python_betterproto2_out=oxia/internal/proto oxia/internal/proto/client.proto
|
oxia-0.1.0/pydoctor.ini
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[pydoctor]
|
|
2
|
+
project-name = Oxia
|
|
3
|
+
project-url = https://github.com/oxia-db/oxia-client-python
|
|
4
|
+
make-html = True
|
|
5
|
+
html-output = docs/api
|
|
6
|
+
add-package = oxia
|
|
7
|
+
privacy =
|
|
8
|
+
HIDDEN:oxia.internal
|
|
9
|
+
HIDDEN:oxia.client_test
|
|
10
|
+
HIDDEN:oxia.client
|
|
11
|
+
HIDDEN:oxia.api
|
|
12
|
+
HIDDEN:oxia.proto
|
|
13
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
[project]
|
|
16
|
+
name = "oxia"
|
|
17
|
+
description = "Oxia client for Python"
|
|
18
|
+
authors = [
|
|
19
|
+
{ name = "StreamNative"}
|
|
20
|
+
]
|
|
21
|
+
dynamic = ["version"]
|
|
22
|
+
|
|
23
|
+
requires-python = ">=3.13"
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"betterproto2-compiler",
|
|
27
|
+
"grpcio",
|
|
28
|
+
"xxhash",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"grpcio-tools>=1.75.1",
|
|
34
|
+
"pydoctor>=25.10.1",
|
|
35
|
+
"pytest>=8.4.2",
|
|
36
|
+
"testcontainers>=4.13.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = [
|
|
41
|
+
"uv_build>=0.8.22,<0.9.0",
|
|
42
|
+
"hatchling",
|
|
43
|
+
"hatch-vcs",
|
|
44
|
+
]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.version]
|
|
48
|
+
source = "vcs"
|
|
@@ -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
|
+
]
|
|
@@ -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()
|