etcd-client-py 0.2.4__tar.gz → 0.3.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 (29) hide show
  1. etcd_client_py-0.3.0/.github/workflows/test.yml +41 -0
  2. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/Cargo.lock +1 -1
  3. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/Cargo.toml +1 -1
  4. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/Makefile +1 -1
  5. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/PKG-INFO +2 -2
  6. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/etcd_client.pyi +28 -36
  7. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/communicator.rs +34 -66
  8. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/compare.rs +14 -6
  9. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/error.rs +4 -4
  10. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/lib.rs +4 -4
  11. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/lock_manager.rs +16 -15
  12. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/txn.rs +9 -4
  13. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/watch.rs +2 -2
  14. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/watch_event.rs +9 -9
  15. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/tests/harness.py +28 -24
  16. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/.github/workflows/ci.yml +0 -0
  17. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/.gitignore +0 -0
  18. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/LICENSE +0 -0
  19. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/README.md +0 -0
  20. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/py.typed +0 -0
  21. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/pyproject.toml +0 -0
  22. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/pytest.ini +0 -0
  23. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/requirements.txt +0 -0
  24. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/client.rs +0 -0
  25. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/condvar.rs +0 -0
  26. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/txn_response.rs +0 -0
  27. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/src/watch_event_stream.rs +0 -0
  28. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/tests/conftest.py +0 -0
  29. {etcd_client_py-0.2.4 → etcd_client_py-0.3.0}/tests/test.py +0 -0
@@ -0,0 +1,41 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v3
11
+ - name: Install etcd
12
+ run: |
13
+ wget https://github.com/etcd-io/etcd/releases/download/v3.4.10/etcd-v3.4.10-linux-amd64.tar.gz
14
+ tar -xzf etcd-v3.4.10-linux-amd64.tar.gz
15
+ mv etcd-v3.4.10-linux-amd64/etcd* /usr/local/bin/
16
+
17
+ - name: Set up Python as Runtime
18
+ uses: actions/setup-python@v4
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Set up Rust toolchain
23
+ uses: actions-rs/toolchain@v1
24
+ with:
25
+ toolchain: stable
26
+ override: true
27
+
28
+ - name: Install protobuf compiler
29
+ run: |
30
+ sudo apt install -y protobuf-compiler
31
+
32
+ - name: Install dependencies and build the package
33
+ run: |
34
+ python -m pip install --upgrade pip
35
+ python -m pip install -r requirements.txt
36
+ python -m maturin build
37
+ python -m pip install .
38
+
39
+ - name: Test
40
+ run: |
41
+ python -m pytest
@@ -210,7 +210,7 @@ dependencies = [
210
210
 
211
211
  [[package]]
212
212
  name = "etcd-client-py"
213
- version = "0.2.4"
213
+ version = "0.3.0"
214
214
  dependencies = [
215
215
  "etcd-client",
216
216
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "etcd-client-py"
3
- version = "0.2.4"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
5
  authors = ["Lablup Inc."]
6
6
  readme = "./README.md"
@@ -12,4 +12,4 @@ fmt:
12
12
  cargo fmt
13
13
 
14
14
  lint:
15
- cargo clippy
15
+ cargo clippy
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: etcd-client-py
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Python
6
6
  License-File: LICENSE
@@ -8,7 +8,7 @@ from typing import Any, AsyncIterator, Final, Optional
8
8
 
9
9
  @dataclass
10
10
  class EtcdLockOption:
11
- lock_name: str
11
+ lock_name: bytes
12
12
  timeout: Optional[float]
13
13
  ttl: Optional[int]
14
14
 
@@ -30,31 +30,31 @@ class CompareOp:
30
30
 
31
31
  class Compare:
32
32
  @staticmethod
33
- def version(key: str, cmp: "CompareOp", version: int) -> "Compare": ...
33
+ def version(key: bytes, cmp: "CompareOp", version: int) -> "Compare": ...
34
34
  """
35
35
  Compares the version of the given key.
36
36
  """
37
37
  @staticmethod
38
- def create_revision(key: str, cmp: "CompareOp", revision: int) -> "Compare": ...
38
+ def create_revision(key: bytes, cmp: "CompareOp", revision: int) -> "Compare": ...
39
39
  """
40
40
  Compares the creation revision of the given key.
41
41
  """
42
42
  @staticmethod
43
- def mod_revision(key: str, cmp: "CompareOp", revision: int) -> "Compare": ...
43
+ def mod_revision(key: bytes, cmp: "CompareOp", revision: int) -> "Compare": ...
44
44
  """
45
45
  Compares the last modified revision of the given key.
46
46
  """
47
47
  @staticmethod
48
- def value(key: str, cmp: "CompareOp", value: str) -> "Compare": ...
48
+ def value(key: bytes, cmp: "CompareOp", value: bytes) -> "Compare": ...
49
49
  """
50
50
  Compares the value of the given key.
51
51
  """
52
52
  @staticmethod
53
- def lease(key: str, cmp: "CompareOp", lease: int) -> "Compare": ...
53
+ def lease(key: bytes, cmp: "CompareOp", lease: int) -> "Compare": ...
54
54
  """
55
55
  Compares the lease id of the given key.
56
56
  """
57
- def with_range(self, end: list[int]) -> "Compare": ...
57
+ def with_range(self, end: bytes) -> "Compare": ...
58
58
  """
59
59
  Sets the comparison to scan the range [key, end).
60
60
  """
@@ -95,11 +95,11 @@ class TxnOp:
95
95
  """
96
96
 
97
97
  @staticmethod
98
- def get(key: str) -> "TxnOp": ...
98
+ def get(key: bytes) -> "TxnOp": ...
99
99
  @staticmethod
100
- def put(key: str, value: str) -> "TxnOp": ...
100
+ def put(key: bytes, value: bytes) -> "TxnOp": ...
101
101
  @staticmethod
102
- def delete(key: str) -> "TxnOp": ...
102
+ def delete(key: bytes) -> "TxnOp": ...
103
103
  @staticmethod
104
104
  def txn(txn: "Txn") -> "TxnOp": ...
105
105
 
@@ -154,21 +154,15 @@ class CondVar:
154
154
  """ """
155
155
 
156
156
  class Communicator:
157
- async def get(self, key: str) -> str:
157
+ async def get(self, key: bytes) -> list[int]:
158
158
  """
159
159
  Gets the key from the key-value store.
160
160
  """
161
- async def get_prefix(self, key: str) -> dict[str, Any]:
161
+ async def get_prefix(self, key: bytes) -> list[tuple[list[int], list[int]]]:
162
162
  """
163
163
  Gets the key from the key-value store.
164
164
  """
165
- async def put(self, key: str, value: str) -> None:
166
- """
167
- Put the given key into the key-value store.
168
- A put request increments the revision of the key-value store
169
- and generates one event in the event history.
170
- """
171
- async def put_prefix(self, key: str, value: dict[str, Any]) -> None:
165
+ async def put(self, key: bytes, value: bytes) -> None:
172
166
  """
173
167
  Put the given key into the key-value store.
174
168
  A put request increments the revision of the key-value store
@@ -181,19 +175,17 @@ class Communicator:
181
175
  and generates events with the same revision for every completed operation.
182
176
  It is not allowed to modify the same key several times within one txn.
183
177
  """
184
- async def delete(self, key: str) -> None:
178
+ async def delete(self, key: bytes) -> None:
185
179
  """
186
180
  Deletes the given key from the key-value store.
187
181
  """
188
- async def delete_prefix(self, key: str) -> None:
182
+ async def delete_prefix(self, key: bytes) -> None:
189
183
  """
190
184
  Deletes the given key from the key-value store.
191
185
  """
192
- async def keys_prefix(self, key: str) -> list[str]:
193
- """ """
194
- async def replace(self, key: str, initial_value: str, new_value: str) -> bool:
186
+ async def keys_prefix(self, key: bytes) -> list[list[int]]:
195
187
  """ """
196
- async def lock(self, name: str) -> None:
188
+ async def lock(self, name: bytes) -> None:
197
189
  """
198
190
  Lock acquires a distributed shared lock on a given named lock.
199
191
  On success, it will return a unique key that exists so long as the
@@ -202,7 +194,7 @@ class Communicator:
202
194
  lock ownership. The lock is held until Unlock is called on the key or the
203
195
  lease associate with the owner expires.
204
196
  """
205
- async def unlock(self, key: str) -> None:
197
+ async def unlock(self, name: bytes) -> None:
206
198
  """
207
199
  Unlock takes a key returned by Lock and releases the hold on lock. The
208
200
  next Lock caller waiting for the lock will then be woken up and given
@@ -225,7 +217,7 @@ class Communicator:
225
217
  """
226
218
  def watch(
227
219
  self,
228
- key: str,
220
+ key: bytes,
229
221
  *,
230
222
  once: Optional[bool] = False,
231
223
  ready_event: Optional["CondVar"] = None,
@@ -238,7 +230,7 @@ class Communicator:
238
230
  """
239
231
  def watch_prefix(
240
232
  self,
241
- key: str,
233
+ key: bytes,
242
234
  *,
243
235
  once: Optional[bool] = False,
244
236
  ready_event: Optional["CondVar"] = None,
@@ -261,16 +253,16 @@ class Watch:
261
253
  class WatchEvent:
262
254
  """ """
263
255
 
264
- key: str
265
- value: str
256
+ key: bytes
257
+ value: bytes
266
258
  event: "WatchEventType"
267
- prev_value: Optional[str]
259
+ prev_value: Optional[bytes]
268
260
 
269
261
  def __init__(
270
- key: str,
271
- value: str,
262
+ key: bytes,
263
+ value: bytes,
272
264
  event: "WatchEventType",
273
- prev_value: Optional[str] = None,
265
+ prev_value: Optional[bytes] = None,
274
266
  ) -> None: ...
275
267
 
276
268
  class WatchEventType:
@@ -296,7 +288,7 @@ class CondVar:
296
288
  class ClientError(Exception):
297
289
  """ """
298
290
 
299
- class GRpcStatusError(ClientError):
291
+ class GRPCStatusError(ClientError):
300
292
  """ """
301
293
 
302
294
  class InvalidArgsError(ClientError):
@@ -332,7 +324,7 @@ class EndpointError(ClientError):
332
324
  class LockError(ClientError):
333
325
  """ """
334
326
 
335
- class GRpcStatusCode(Enum):
327
+ class GRPCStatusCode(Enum):
336
328
  Ok = 0
337
329
  """The operation completed successfully."""
338
330
 
@@ -1,9 +1,8 @@
1
- use etcd_client::{Client as EtcdClient, PutOptions};
1
+ use etcd_client::Client as EtcdClient;
2
2
  use etcd_client::{DeleteOptions, GetOptions, WatchOptions};
3
- use pyo3::exceptions::PyException;
4
3
  use pyo3::prelude::*;
4
+ use pyo3::types::PyBytes;
5
5
  use pyo3_asyncio::tokio::future_into_py;
6
- use std::collections::HashMap;
7
6
  use std::sync::Arc;
8
7
  use tokio::sync::Mutex;
9
8
 
@@ -19,8 +18,9 @@ pub struct PyCommunicator(pub Arc<Mutex<EtcdClient>>);
19
18
  #[pymethods]
20
19
  impl PyCommunicator {
21
20
  // TODO: Implement and use the CRUD response types
22
- fn get<'a>(&'a self, py: Python<'a>, key: String) -> PyResult<&'a PyAny> {
21
+ fn get<'a>(&'a self, py: Python<'a>, key: &PyBytes) -> PyResult<&'a PyAny> {
23
22
  let client = self.0.clone();
23
+ let key = key.as_bytes().to_vec();
24
24
  future_into_py(py, async move {
25
25
  let mut client = client.lock().await;
26
26
  let result = client.get(key, None).await;
@@ -28,7 +28,7 @@ impl PyCommunicator {
28
28
  .map(|response| {
29
29
  let kvs = response.kvs();
30
30
  if !kvs.is_empty() {
31
- Some(String::from_utf8(kvs[0].value().to_owned()).unwrap())
31
+ Some(kvs[0].value().to_owned())
32
32
  } else {
33
33
  None
34
34
  }
@@ -37,29 +37,32 @@ impl PyCommunicator {
37
37
  })
38
38
  }
39
39
 
40
- fn get_prefix<'a>(&'a self, py: Python<'a>, prefix: String) -> PyResult<&'a PyAny> {
40
+ fn get_prefix<'a>(&'a self, py: Python<'a>, prefix: &PyBytes) -> PyResult<&'a PyAny> {
41
41
  let client = self.0.clone();
42
+ let prefix = prefix.as_bytes().to_vec();
43
+
42
44
  future_into_py(py, async move {
43
45
  let mut client = client.lock().await;
44
46
  let options = GetOptions::new().with_prefix();
45
47
  let result = client.get(prefix, Some(options)).await;
46
48
  result
47
49
  .map(|response| {
48
- let mut result = HashMap::new();
50
+ let mut list = vec![];
49
51
  let kvs = response.kvs();
50
52
  for kv in kvs {
51
- let key = String::from_utf8(kv.key().to_owned()).unwrap();
52
- let value = String::from_utf8(kv.value().to_owned()).unwrap();
53
- result.insert(key, value);
53
+ list.push((kv.key().to_owned(), kv.value().to_owned()));
54
54
  }
55
- result
55
+ list
56
56
  })
57
57
  .map_err(|e| PyClientError(e).into())
58
58
  })
59
59
  }
60
60
 
61
- fn put<'a>(&'a self, py: Python<'a>, key: String, value: String) -> PyResult<&'a PyAny> {
61
+ fn put<'a>(&'a self, py: Python<'a>, key: &PyBytes, value: &PyBytes) -> PyResult<&'a PyAny> {
62
62
  let client = self.0.clone();
63
+ let key = key.as_bytes().to_vec();
64
+ let value = value.as_bytes().to_vec();
65
+
63
66
  future_into_py(py, async move {
64
67
  let mut client = client.lock().await;
65
68
  let result = client.put(key, value, None).await;
@@ -67,23 +70,10 @@ impl PyCommunicator {
67
70
  })
68
71
  }
69
72
 
70
- fn put_prefix<'a>(
71
- &'a self,
72
- py: Python<'a>,
73
- prefix: String,
74
- value: String,
75
- ) -> PyResult<&'a PyAny> {
73
+ fn delete<'a>(&'a self, py: Python<'a>, key: &PyBytes) -> PyResult<&'a PyAny> {
76
74
  let client = self.0.clone();
77
- future_into_py(py, async move {
78
- let mut client = client.lock().await;
79
- let options = PutOptions::new().with_prev_key();
80
- let result = client.put(prefix, value, Some(options)).await;
81
- result.map(|_| ()).map_err(|e| PyClientError(e).into())
82
- })
83
- }
75
+ let key = key.as_bytes().to_vec();
84
76
 
85
- fn delete<'a>(&'a self, py: Python<'a>, key: String) -> PyResult<&'a PyAny> {
86
- let client = self.0.clone();
87
77
  future_into_py(py, async move {
88
78
  let mut client = client.lock().await;
89
79
 
@@ -92,8 +82,10 @@ impl PyCommunicator {
92
82
  })
93
83
  }
94
84
 
95
- fn delete_prefix<'a>(&'a self, py: Python<'a>, key: String) -> PyResult<&'a PyAny> {
85
+ fn delete_prefix<'a>(&'a self, py: Python<'a>, key: &PyBytes) -> PyResult<&'a PyAny> {
96
86
  let client = self.0.clone();
87
+ let key = key.as_bytes().to_vec();
88
+
97
89
  future_into_py(py, async move {
98
90
  let mut client = client.lock().await;
99
91
  let options = DeleteOptions::new().with_prefix();
@@ -114,39 +106,10 @@ impl PyCommunicator {
114
106
  })
115
107
  }
116
108
 
117
- fn replace<'a>(
118
- &'a self,
119
- py: Python<'a>,
120
- key: String,
121
- initial_val: String,
122
- new_val: String,
123
- ) -> PyResult<&'a PyAny> {
109
+ fn keys_prefix<'a>(&'a self, py: Python<'a>, key: &PyBytes) -> PyResult<&'a PyAny> {
124
110
  let client = self.0.clone();
125
- future_into_py(py, async move {
126
- let mut client = client.lock().await;
127
- match client.get(key.clone(), None).await {
128
- Ok(response) => {
129
- if let Some(key_value) = response.kvs().get(0) {
130
- if *key_value.value_str().unwrap() == initial_val {
131
- match client.put(key, new_val, None).await {
132
- Ok(_) => Ok(true), // replace successful
133
- Err(e) => Err(PyClientError(e)),
134
- }
135
- } else {
136
- Ok(false) // initial_val not equal to current value
137
- }
138
- } else {
139
- Ok(false) // Key does not exist
140
- }
141
- }
142
- Err(e) => Err(PyClientError(e)),
143
- }
144
- .map_err(|e| PyErr::new::<PyException, _>(format!("{}", e.0)))
145
- })
146
- }
111
+ let key = key.as_bytes().to_vec();
147
112
 
148
- fn keys_prefix<'a>(&'a self, py: Python<'a>, key: String) -> PyResult<&'a PyAny> {
149
- let client = self.0.clone();
150
113
  future_into_py(py, async move {
151
114
  let mut client = client.lock().await;
152
115
  let options = GetOptions::new().with_prefix();
@@ -156,8 +119,7 @@ impl PyCommunicator {
156
119
  let mut result = Vec::new();
157
120
  let kvs = response.kvs();
158
121
  for kv in kvs {
159
- let key = String::from_utf8(kv.key().to_owned()).unwrap();
160
- result.push(key);
122
+ result.push(kv.key().to_owned());
161
123
  }
162
124
  result
163
125
  })
@@ -165,8 +127,10 @@ impl PyCommunicator {
165
127
  })
166
128
  }
167
129
 
168
- fn lock<'a>(&'a self, py: Python<'a>, name: String) -> PyResult<&'a PyAny> {
130
+ fn lock<'a>(&'a self, py: Python<'a>, name: &PyBytes) -> PyResult<&'a PyAny> {
169
131
  let client = self.0.clone();
132
+ let name = name.as_bytes().to_vec();
133
+
170
134
  future_into_py(py, async move {
171
135
  let mut client = client.lock().await;
172
136
  let result = client.lock(name, None).await;
@@ -174,11 +138,13 @@ impl PyCommunicator {
174
138
  })
175
139
  }
176
140
 
177
- fn unlock<'a>(&'a self, py: Python<'a>, key: String) -> PyResult<&'a PyAny> {
141
+ fn unlock<'a>(&'a self, py: Python<'a>, name: &PyBytes) -> PyResult<&'a PyAny> {
178
142
  let client = self.0.clone();
143
+ let name = name.as_bytes().to_vec();
144
+
179
145
  future_into_py(py, async move {
180
146
  let mut client = client.lock().await;
181
- let result = client.unlock(key).await;
147
+ let result = client.unlock(name).await;
182
148
  result.map(|_| ()).map_err(|e| PyClientError(e).into())
183
149
  })
184
150
  }
@@ -222,24 +188,26 @@ impl PyCommunicator {
222
188
 
223
189
  fn watch(
224
190
  &self,
225
- key: String,
191
+ key: &PyBytes,
226
192
  once: Option<bool>,
227
193
  ready_event: Option<PyCondVar>,
228
194
  cleanup_event: Option<PyCondVar>,
229
195
  ) -> PyWatch {
230
196
  let client = self.0.clone();
197
+ let key = key.as_bytes().to_vec();
231
198
  let once = once.unwrap_or(false);
232
199
  PyWatch::new(client, key, once, None, ready_event, cleanup_event)
233
200
  }
234
201
 
235
202
  fn watch_prefix(
236
203
  &self,
237
- key: String,
204
+ key: &PyBytes,
238
205
  once: Option<bool>,
239
206
  ready_event: Option<PyCondVar>,
240
207
  cleanup_event: Option<PyCondVar>,
241
208
  ) -> PyWatch {
242
209
  let client = self.0.clone();
210
+ let key = key.as_bytes().to_vec();
243
211
  let once = once.unwrap_or(false);
244
212
  let options = WatchOptions::new().with_prefix();
245
213
  PyWatch::new(client, key, once, Some(options), ready_event, cleanup_event)
@@ -1,6 +1,7 @@
1
1
  use etcd_client::{Compare, CompareOp};
2
2
  use pyo3::prelude::*;
3
3
  use pyo3::pyclass::CompareOp as PyO3CompareOp;
4
+ use pyo3::types::PyBytes;
4
5
 
5
6
  #[derive(Clone)]
6
7
  #[pyclass(name = "CompareOp")]
@@ -42,31 +43,38 @@ pub struct PyCompare(pub Compare);
42
43
  #[pymethods]
43
44
  impl PyCompare {
44
45
  #[staticmethod]
45
- fn version(key: String, cmp: PyCompareOp, version: i64) -> PyResult<Self> {
46
+ fn version(key: &PyBytes, cmp: PyCompareOp, version: i64) -> PyResult<Self> {
47
+ let key = key.as_bytes().to_vec();
46
48
  Ok(PyCompare(Compare::version(key, cmp.0, version)))
47
49
  }
48
50
 
49
51
  #[staticmethod]
50
- fn create_revision(key: String, cmp: PyCompareOp, revision: i64) -> PyResult<Self> {
52
+ fn create_revision(key: &PyBytes, cmp: PyCompareOp, revision: i64) -> PyResult<Self> {
53
+ let key = key.as_bytes().to_vec();
51
54
  Ok(PyCompare(Compare::create_revision(key, cmp.0, revision)))
52
55
  }
53
56
 
54
57
  #[staticmethod]
55
- fn mod_revision(key: String, cmp: PyCompareOp, revision: i64) -> PyResult<Self> {
58
+ fn mod_revision(key: &PyBytes, cmp: PyCompareOp, revision: i64) -> PyResult<Self> {
59
+ let key = key.as_bytes().to_vec();
56
60
  Ok(PyCompare(Compare::mod_revision(key, cmp.0, revision)))
57
61
  }
58
62
 
59
63
  #[staticmethod]
60
- fn value(key: String, cmp: PyCompareOp, value: String) -> PyResult<Self> {
64
+ fn value(key: &PyBytes, cmp: PyCompareOp, value: &PyBytes) -> PyResult<Self> {
65
+ let key = key.as_bytes().to_vec();
66
+ let value = value.as_bytes().to_vec();
61
67
  Ok(PyCompare(Compare::value(key, cmp.0, value)))
62
68
  }
63
69
 
64
70
  #[staticmethod]
65
- fn lease(key: String, cmp: PyCompareOp, lease: i64) -> PyResult<Self> {
71
+ fn lease(key: &PyBytes, cmp: PyCompareOp, lease: i64) -> PyResult<Self> {
72
+ let key = key.as_bytes().to_vec();
66
73
  Ok(PyCompare(Compare::lease(key, cmp.0, lease)))
67
74
  }
68
75
 
69
- fn with_range(&self, end: String) -> PyResult<Self> {
76
+ fn with_range(&self, end: &PyBytes) -> PyResult<Self> {
77
+ let end = end.as_bytes().to_vec();
70
78
  Ok(PyCompare(self.0.clone().with_range(end)))
71
79
  }
72
80
 
@@ -2,7 +2,7 @@ use pyo3::{create_exception, exceptions::PyException, types::PyDict, PyErr, Pyth
2
2
  use std::fmt::Debug;
3
3
 
4
4
  create_exception!(etcd_client, ClientError, PyException);
5
- create_exception!(etcd_client, GRpcStatusError, ClientError);
5
+ create_exception!(etcd_client, GRPCStatusError, ClientError);
6
6
  create_exception!(etcd_client, InvalidArgsError, ClientError);
7
7
  create_exception!(etcd_client, InvalidUriError, ClientError);
8
8
  create_exception!(etcd_client, IoError, ClientError);
@@ -15,8 +15,8 @@ create_exception!(etcd_client, InvalidHeaderValueError, ClientError);
15
15
  create_exception!(etcd_client, EndpointError, ClientError);
16
16
  create_exception!(etcd_client, LockError, ClientError);
17
17
 
18
- #[pyclass(name = "GRpcStatusCode")]
19
- pub enum PyGRpcStatusCode {
18
+ #[pyclass(name = "GRPCStatusCode")]
19
+ pub enum PyGRPCStatusCode {
20
20
  Ok = 0,
21
21
  Cancelled = 1,
22
22
  Unknown = 2,
@@ -54,7 +54,7 @@ impl From<PyClientError> for PyErr {
54
54
  .unwrap();
55
55
 
56
56
  let kv_args: PyObject = error_details.into_py(py);
57
- GRpcStatusError::new_err(kv_args)
57
+ GRPCStatusError::new_err(kv_args)
58
58
  }),
59
59
  etcd_client::Error::InvalidArgs(e) => {
60
60
  InvalidArgsError::new_err(format!("InvalidArgsError(err={})", e))
@@ -15,8 +15,8 @@ use communicator::PyCommunicator;
15
15
  use compare::{PyCompare, PyCompareOp};
16
16
  use condvar::PyCondVar;
17
17
  use error::{
18
- ClientError, ElectError, EndpointError, GRpcStatusError, InvalidArgsError,
19
- InvalidHeaderValueError, InvalidUriError, IoError, LeaseKeepAliveError, PyGRpcStatusCode,
18
+ ClientError, ElectError, EndpointError, GRPCStatusError, InvalidArgsError,
19
+ InvalidHeaderValueError, InvalidUriError, IoError, LeaseKeepAliveError, PyGRPCStatusCode,
20
20
  TransportError, Utf8Error, WatchError,
21
21
  };
22
22
  use lock_manager::PyEtcdLockOption;
@@ -45,10 +45,10 @@ fn etcd_client(py: Python, module: &PyModule) -> PyResult<()> {
45
45
  module.add_class::<PyTxnResponse>()?;
46
46
  module.add_class::<PyEtcdLockOption>()?;
47
47
 
48
- module.add_class::<PyGRpcStatusCode>()?;
48
+ module.add_class::<PyGRPCStatusCode>()?;
49
49
 
50
50
  module.add("ClientError", py.get_type::<ClientError>())?;
51
- module.add("GRpcStatusError", py.get_type::<GRpcStatusError>())?;
51
+ module.add("GRPCStatusError", py.get_type::<GRPCStatusError>())?;
52
52
  module.add("InvalidArgsError", py.get_type::<InvalidArgsError>())?;
53
53
  module.add("IoError", py.get_type::<IoError>())?;
54
54
  module.add("InvalidUriError", py.get_type::<InvalidUriError>())?;
@@ -1,18 +1,18 @@
1
1
  use crate::{
2
2
  client::PyClient,
3
3
  communicator::PyCommunicator,
4
- error::{GRpcStatusError, LockError, PyClientError},
4
+ error::{GRPCStatusError, LockError, PyClientError},
5
5
  };
6
6
  use etcd_client::{Client as EtcdClient, LockOptions};
7
7
 
8
- use pyo3::prelude::*;
8
+ use pyo3::{prelude::*, types::PyBytes};
9
9
  use std::time::Duration;
10
10
  use tokio::time::{sleep, timeout};
11
11
 
12
12
  #[derive(Debug, Clone)]
13
13
  #[pyclass(get_all, set_all, name = "EtcdLockOption")]
14
14
  pub struct PyEtcdLockOption {
15
- pub lock_name: String,
15
+ pub lock_name: Vec<u8>,
16
16
  pub timeout: Option<f64>,
17
17
  pub ttl: Option<i64>,
18
18
  }
@@ -20,7 +20,8 @@ pub struct PyEtcdLockOption {
20
20
  #[pymethods]
21
21
  impl PyEtcdLockOption {
22
22
  #[new]
23
- fn new(lock_name: String, timeout: Option<f64>, ttl: Option<i64>) -> Self {
23
+ fn new(lock_name: &PyBytes, timeout: Option<f64>, ttl: Option<i64>) -> Self {
24
+ let lock_name = lock_name.as_bytes().to_vec();
24
25
  Self {
25
26
  lock_name,
26
27
  timeout,
@@ -30,7 +31,7 @@ impl PyEtcdLockOption {
30
31
 
31
32
  fn __repr__(&self) -> PyResult<String> {
32
33
  Ok(format!(
33
- "EtcdLockOption(lock_name={}, timeout={:?}, ttl={:?})",
34
+ "EtcdLockOption(lock_name={:?}, timeout={:?}, ttl={:?})",
34
35
  self.lock_name, self.timeout, self.ttl
35
36
  ))
36
37
  }
@@ -38,10 +39,10 @@ impl PyEtcdLockOption {
38
39
 
39
40
  pub struct EtcdLockManager {
40
41
  pub client: PyClient,
41
- pub lock_name: String,
42
+ pub lock_name: Vec<u8>,
42
43
  pub ttl: Option<i64>,
43
44
  pub timeout_seconds: Option<f64>,
44
- pub lock_id: Option<String>,
45
+ pub lock_id: Option<Vec<u8>>,
45
46
  pub lease_id: Option<i64>,
46
47
  pub lease_keepalive_task: Option<tokio::task::JoinHandle<Result<(), PyClientError>>>,
47
48
  }
@@ -98,15 +99,11 @@ impl EtcdLockManager {
98
99
  .map(|lease_id| LockOptions::new().with_lease(lease_id));
99
100
 
100
101
  let lock_res = client
101
- .lock(self.lock_name.clone().as_bytes(), lock_req_options)
102
+ .lock(self.lock_name.clone(), lock_req_options)
102
103
  .await
103
104
  .map_err(PyClientError)?;
104
105
 
105
- self.lock_id = Some(
106
- std::str::from_utf8(lock_res.key())
107
- .expect("Invalid utf8 chars includeded in lock request!")
108
- .to_owned(),
109
- );
106
+ self.lock_id = Some(lock_res.key().to_vec());
110
107
  Ok(())
111
108
  },
112
109
  )
@@ -123,7 +120,7 @@ impl EtcdLockManager {
123
120
  etcd_client::Error::GRpcStatus(status)
124
121
  if status.code() != tonic::Code::NotFound =>
125
122
  {
126
- return Err(GRpcStatusError::new_err(status.to_string()))
123
+ return Err(GRPCStatusError::new_err(status.to_string()))
127
124
  }
128
125
  _ => return Err(PyClientError(e).into()),
129
126
  },
@@ -160,7 +157,11 @@ impl EtcdLockManager {
160
157
  client.lease_revoke(lease_id).await.map_err(PyClientError)?;
161
158
  } else {
162
159
  client
163
- .unlock(self.lock_id.as_ref().unwrap().as_bytes())
160
+ .unlock(
161
+ self.lock_id
162
+ .to_owned()
163
+ .expect("Failed to unlock due to lock_id is None"),
164
+ )
164
165
  .await
165
166
  .map_err(PyClientError)?;
166
167
  }
@@ -1,5 +1,5 @@
1
1
  use etcd_client::{DeleteOptions, GetOptions, PutOptions, Txn, TxnOp};
2
- use pyo3::prelude::*;
2
+ use pyo3::{prelude::*, types::PyBytes};
3
3
 
4
4
  use crate::compare::PyCompare;
5
5
 
@@ -10,18 +10,23 @@ pub struct PyTxnOp(pub TxnOp);
10
10
  #[pymethods]
11
11
  impl PyTxnOp {
12
12
  #[staticmethod]
13
- fn get(key: String) -> PyResult<Self> {
13
+ fn get(key: &PyBytes) -> PyResult<Self> {
14
+ let key = key.as_bytes().to_vec();
14
15
  let options = GetOptions::new();
15
16
  Ok(PyTxnOp(TxnOp::get(key, Some(options))))
16
17
  }
18
+
17
19
  #[staticmethod]
18
- fn put(key: String, value: String) -> PyResult<Self> {
20
+ fn put(key: &PyBytes, value: &PyBytes) -> PyResult<Self> {
21
+ let key = key.as_bytes().to_vec();
22
+ let value = value.as_bytes().to_vec();
19
23
  let options = PutOptions::new();
20
24
  Ok(PyTxnOp(TxnOp::put(key, value, Some(options))))
21
25
  }
22
26
 
23
27
  #[staticmethod]
24
- fn delete(key: String) -> PyResult<Self> {
28
+ fn delete(key: &PyBytes) -> PyResult<Self> {
29
+ let key = key.as_bytes().to_vec();
25
30
  let options = DeleteOptions::new();
26
31
  Ok(PyTxnOp(TxnOp::delete(key, Some(options))))
27
32
  }
@@ -16,7 +16,7 @@ use crate::watch_event_stream::PyWatchEventStream;
16
16
  #[derive(Clone)]
17
17
  pub struct PyWatch {
18
18
  client: Arc<Mutex<EtcdClient>>,
19
- key: String,
19
+ key: Vec<u8>,
20
20
  once: bool,
21
21
  options: Option<WatchOptions>,
22
22
  watcher: Arc<Mutex<Option<Watcher>>>,
@@ -30,7 +30,7 @@ pub struct PyWatch {
30
30
  impl PyWatch {
31
31
  pub fn new(
32
32
  client: Arc<Mutex<EtcdClient>>,
33
- key: String,
33
+ key: Vec<u8>,
34
34
  once: bool,
35
35
  options: Option<WatchOptions>,
36
36
  ready_event: Option<PyCondVar>,
@@ -7,10 +7,10 @@ use pyo3::pyclass::CompareOp;
7
7
  #[pyclass(get_all, name = "WatchEvent")]
8
8
  #[derive(PartialEq, Eq, Clone, Debug)]
9
9
  pub struct PyWatchEvent {
10
- key: String,
11
- value: String,
10
+ key: Vec<u8>,
11
+ value: Vec<u8>,
12
12
  event: PyWatchEventType,
13
- prev_value: Option<String>,
13
+ prev_value: Option<Vec<u8>>,
14
14
  }
15
15
 
16
16
  #[pymethods]
@@ -18,10 +18,10 @@ impl PyWatchEvent {
18
18
  #[new]
19
19
  #[pyo3(signature = (key, value, event, prev_value))]
20
20
  fn new(
21
- key: String,
22
- value: String,
21
+ key: Vec<u8>,
22
+ value: Vec<u8>,
23
23
  event: PyWatchEventType,
24
- prev_value: Option<String>,
24
+ prev_value: Option<Vec<u8>>,
25
25
  ) -> Self {
26
26
  Self {
27
27
  key,
@@ -33,7 +33,7 @@ impl PyWatchEvent {
33
33
 
34
34
  pub fn __repr__(&self) -> String {
35
35
  format!(
36
- "Event(event={:?}, key={}, value={}, prev_value={:?})",
36
+ "Event(event={:?}, key={:?}, value={:?}, prev_value={:?})",
37
37
  self.event, self.key, self.value, self.prev_value
38
38
  )
39
39
  }
@@ -50,8 +50,8 @@ impl PyWatchEvent {
50
50
  impl From<EtcdClientEvent> for PyWatchEvent {
51
51
  fn from(event: EtcdClientEvent) -> Self {
52
52
  let kv = event.kv().unwrap();
53
- let key = String::from_utf8(kv.key().to_owned()).unwrap();
54
- let value = String::from_utf8(kv.value().to_owned()).unwrap();
53
+ let key = kv.key().to_owned();
54
+ let value = kv.value().to_owned();
55
55
  let prev_value = None;
56
56
  let event = PyWatchEventType(event.event_type());
57
57
  Self {
@@ -1,3 +1,9 @@
1
+ """
2
+ Backend.AI AsyncEtcd Client Copy
3
+ Ref: https://github.com/lablup/backend.ai/blob/main/src/ai/backend/common/etcd.py
4
+ =================================================================================
5
+ """
6
+
1
7
  """
2
8
  An asynchronous client wrapper for etcd v3 API.
3
9
 
@@ -7,8 +13,6 @@ Fortunately, etcd3's watchers are not blocking because they are implemented
7
13
  using callbacks in separate threads.
8
14
  """
9
15
 
10
- from __future__ import annotations
11
-
12
16
  import asyncio
13
17
  from dataclasses import dataclass
14
18
  import enum
@@ -44,8 +48,8 @@ from etcd_client import (
44
48
  CompareOp,
45
49
  CondVar,
46
50
  ConnectOptions,
47
- GRpcStatusCode,
48
- GRpcStatusError,
51
+ GRPCStatusCode,
52
+ GRPCStatusError,
49
53
  WatchEvent,
50
54
  )
51
55
 
@@ -64,7 +68,7 @@ class HostPortPair:
64
68
  return f"{self.host}:{self.port}"
65
69
 
66
70
  @classmethod
67
- def parse(cls, s: str) -> HostPortPair:
71
+ def parse(cls, s: str) -> "HostPortPair":
68
72
  if ":" in s:
69
73
  host, port_str = s.rsplit(":")
70
74
  port = int(port_str)
@@ -220,7 +224,7 @@ class AsyncEtcd:
220
224
  scope_prefix = self._merge_scope_prefix_map(scope_prefix_map)[scope]
221
225
  mangled_key = self._mangle_key(f"{_slash(scope_prefix)}{key}")
222
226
  async with self.etcd.connect() as communicator:
223
- await communicator.put(mangled_key, str(val))
227
+ await communicator.put(mangled_key.encode(self.encoding), str(val).encode(self.encoding))
224
228
 
225
229
  async def put_prefix(
226
230
  self,
@@ -260,7 +264,7 @@ class AsyncEtcd:
260
264
  actions = []
261
265
  for k, v in flattened_dict.items():
262
266
  actions.append(
263
- TxnOp.put(self._mangle_key(f"{_slash(scope_prefix)}{k}"), str(v))
267
+ TxnOp.put(self._mangle_key(f"{_slash(scope_prefix)}{k}").encode(self.encoding), str(v).encode(self.encoding))
264
268
  )
265
269
 
266
270
  await communicator.txn(
@@ -289,7 +293,7 @@ class AsyncEtcd:
289
293
  actions = []
290
294
  for k, v in flattened_dict_obj.items():
291
295
  actions.append(
292
- TxnOp.put(self._mangle_key(f"{_slash(scope_prefix)}{k}"), str(v))
296
+ TxnOp.put(self._mangle_key(f"{_slash(scope_prefix)}{k}").encode(self.encoding), str(v).encode(self.encoding))
293
297
  )
294
298
 
295
299
  async with self.etcd.connect() as communicator:
@@ -337,10 +341,10 @@ class AsyncEtcd:
337
341
  async with self.etcd.connect() as communicator:
338
342
  for scope_prefix in scope_prefixes:
339
343
  value = await communicator.get(
340
- self._mangle_key(f"{_slash(scope_prefix)}{key}")
344
+ self._mangle_key(f"{_slash(scope_prefix)}{key}").encode(self.encoding)
341
345
  )
342
346
  if value is not None:
343
- return value
347
+ return bytes(value).decode(self.encoding)
344
348
  return None
345
349
 
346
350
  async def get_prefix(
@@ -409,9 +413,9 @@ class AsyncEtcd:
409
413
  mangled_key_prefix = self._mangle_key(
410
414
  f"{_slash(scope_prefix)}{key_prefix}"
411
415
  )
412
- values = await communicator.get_prefix(mangled_key_prefix)
416
+ values = await communicator.get_prefix(mangled_key_prefix.encode(self.encoding))
413
417
  pair_sets.append(
414
- [(self._demangle_key(k), v) for k, v in values.items()]
418
+ [(self._demangle_key(bytes(k).decode(self.encoding)), bytes(v).decode(self.encoding)) for k, v in values]
415
419
  )
416
420
 
417
421
  configs = [
@@ -440,10 +444,10 @@ class AsyncEtcd:
440
444
  EtcdTransactionAction()
441
445
  .when(
442
446
  [
443
- Compare.value(mangled_key, CompareOp.EQUAL, initial_val),
447
+ Compare.value(mangled_key.encode(self.encoding), CompareOp.EQUAL, initial_val.encode(self.encoding)),
444
448
  ]
445
449
  )
446
- .and_then([TxnOp.put(mangled_key, new_val)])
450
+ .and_then([TxnOp.put(mangled_key.encode(self.encoding), new_val.encode(self.encoding))])
447
451
  .or_else([])
448
452
  )
449
453
 
@@ -459,7 +463,7 @@ class AsyncEtcd:
459
463
  scope_prefix = self._merge_scope_prefix_map(scope_prefix_map)[scope]
460
464
  mangled_key = self._mangle_key(f"{_slash(scope_prefix)}{key}")
461
465
  async with self.etcd.connect() as communicator:
462
- await communicator.delete(mangled_key)
466
+ await communicator.delete(mangled_key.encode(self.encoding))
463
467
 
464
468
  async def delete_multi(
465
469
  self,
@@ -473,7 +477,7 @@ class AsyncEtcd:
473
477
  actions = []
474
478
  for k in keys:
475
479
  actions.append(
476
- TxnOp.delete(self._mangle_key(f"{_slash(scope_prefix)}{k}"))
480
+ TxnOp.delete(self._mangle_key(f"{_slash(scope_prefix)}{k}").encode(self.encoding))
477
481
  )
478
482
  communicator.txn(EtcdTransactionAction().and_then(actions).or_else([]))
479
483
 
@@ -487,7 +491,7 @@ class AsyncEtcd:
487
491
  scope_prefix = self._merge_scope_prefix_map(scope_prefix_map)[scope]
488
492
  mangled_key_prefix = self._mangle_key(f"{_slash(scope_prefix)}{key_prefix}")
489
493
  async with self.etcd.connect() as communicator:
490
- await communicator.delete_prefix(mangled_key_prefix)
494
+ await communicator.delete_prefix(mangled_key_prefix.encode(self.encoding))
491
495
 
492
496
  async def _watch_impl(
493
497
  self,
@@ -508,7 +512,7 @@ class AsyncEtcd:
508
512
  )
509
513
  except asyncio.TimeoutError:
510
514
  pass
511
- yield Event(ev.key[scope_prefix_len:], ev.event, ev.value)
515
+ yield Event(bytes(ev.key).decode(self.encoding)[scope_prefix_len:], ev.event, bytes(ev.value).decode(self.encoding))
512
516
  if once:
513
517
  return
514
518
  finally:
@@ -535,7 +539,7 @@ class AsyncEtcd:
535
539
  try:
536
540
  async for ev in self._watch_impl(
537
541
  lambda communicator: communicator.watch(
538
- mangled_key,
542
+ mangled_key.encode(self.encoding),
539
543
  ready_event=ready_event,
540
544
  ),
541
545
  scope_prefix_len,
@@ -545,10 +549,10 @@ class AsyncEtcd:
545
549
  ):
546
550
  yield ev
547
551
  ended_without_error = True
548
- except GRpcStatusError as e:
552
+ except GRPCStatusError as e:
549
553
  err_detail = e.args[0]
550
554
 
551
- if err_detail["code"] == GRpcStatusCode.Unavailable:
555
+ if err_detail["code"] == GRPCStatusCode.Unavailable:
552
556
  log.warn(
553
557
  "watch(): error while connecting to Etcd server, retrying..."
554
558
  )
@@ -577,7 +581,7 @@ class AsyncEtcd:
577
581
  try:
578
582
  async for ev in self._watch_impl(
579
583
  lambda communicator: communicator.watch_prefix(
580
- mangled_key_prefix,
584
+ mangled_key_prefix.encode(self.encoding),
581
585
  ready_event=ready_event,
582
586
  ),
583
587
  scope_prefix_len,
@@ -587,10 +591,10 @@ class AsyncEtcd:
587
591
  ):
588
592
  yield ev
589
593
  ended_without_error = True
590
- except GRpcStatusError as e:
594
+ except GRPCStatusError as e:
591
595
  err_detail = e.args[0]
592
596
 
593
- if err_detail["code"] == GRpcStatusCode.Unavailable:
597
+ if err_detail["code"] == GRPCStatusCode.Unavailable:
594
598
  log.warn(
595
599
  "watch_prefix(): error while connecting to Etcd server, retrying..."
596
600
  )
File without changes
File without changes
File without changes