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.
Files changed (32) hide show
  1. oxia-0.1.0/.github/workflows/pypi-release.yaml +44 -0
  2. oxia-0.1.0/.github/workflows/run-tests.yaml +43 -0
  3. oxia-0.1.0/.gitignore +7 -0
  4. oxia-0.1.0/.python-version +1 -0
  5. oxia-0.1.0/PKG-INFO +9 -0
  6. oxia-0.1.0/gen-proto.sh +19 -0
  7. oxia-0.1.0/pydoctor.ini +13 -0
  8. oxia-0.1.0/pyproject.toml +48 -0
  9. oxia-0.1.0/src/oxia/__init__.py +38 -0
  10. oxia-0.1.0/src/oxia/client.py +300 -0
  11. oxia-0.1.0/src/oxia/defs.py +165 -0
  12. oxia-0.1.0/src/oxia/ex.py +28 -0
  13. oxia-0.1.0/src/oxia/internal/__init__.py +14 -0
  14. oxia-0.1.0/src/oxia/internal/backoff.py +28 -0
  15. oxia-0.1.0/src/oxia/internal/compare.py +53 -0
  16. oxia-0.1.0/src/oxia/internal/connection_pool.py +36 -0
  17. oxia-0.1.0/src/oxia/internal/notifications.py +115 -0
  18. oxia-0.1.0/src/oxia/internal/oxia_container.py +28 -0
  19. oxia-0.1.0/src/oxia/internal/proto/__init__.py +0 -0
  20. oxia-0.1.0/src/oxia/internal/proto/client.proto +508 -0
  21. oxia-0.1.0/src/oxia/internal/proto/io/__init__.py +0 -0
  22. oxia-0.1.0/src/oxia/internal/proto/io/streamnative/__init__.py +0 -0
  23. oxia-0.1.0/src/oxia/internal/proto/io/streamnative/oxia/__init__.py +0 -0
  24. oxia-0.1.0/src/oxia/internal/proto/io/streamnative/oxia/proto/__init__.py +1135 -0
  25. oxia-0.1.0/src/oxia/internal/proto/message_pool.py +3 -0
  26. oxia-0.1.0/src/oxia/internal/proto/py.typed +0 -0
  27. oxia-0.1.0/src/oxia/internal/sequence_updates.py +75 -0
  28. oxia-0.1.0/src/oxia/internal/service_discovery.py +124 -0
  29. oxia-0.1.0/src/oxia/internal/sessions.py +73 -0
  30. oxia-0.1.0/tests/client_test.py +740 -0
  31. oxia-0.1.0/tests/compare_test.py +46 -0
  32. 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,7 @@
1
+ .idea
2
+ .DS_Store
3
+ __pycache__
4
+ docs/api
5
+ build
6
+ dist
7
+ *.egg-info
@@ -0,0 +1 @@
1
+ 3.13
oxia-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: oxia
3
+ Version: 0.1.0
4
+ Summary: Oxia client for Python
5
+ Author: StreamNative
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: betterproto2-compiler
8
+ Requires-Dist: grpcio
9
+ Requires-Dist: xxhash
@@ -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
@@ -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()