kachedb 0.1.0a2__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.
kachedb/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ """
2
+ KacheDB — Python client for the KacheDB zero-copy storage engine.
3
+
4
+ Install::
5
+
6
+ pip install kachedb
7
+ pip install kachedb[torch] # for PyTorch tensor support
8
+
9
+ Quickstart::
10
+
11
+ from kachedb import KacheClient
12
+
13
+ with KacheClient(host="127.0.0.1", port=6379) as client:
14
+ client.set("user:1", "alice", ex=3600)
15
+ print(client.get("user:1")) # b"alice"
16
+
17
+ Async::
18
+
19
+ from kachedb import AsyncKacheClient
20
+
21
+ async with AsyncKacheClient() as client:
22
+ await client.set("key", "value")
23
+ print(await client.get("key"))
24
+ """
25
+
26
+ from ._version import __version__
27
+ from .async_client import AsyncKacheClient
28
+ from .client import KacheClient
29
+ from .connection import Connection
30
+ from .descriptor import TENSOR_DESCRIPTOR_MAGIC, TensorBlockDescriptor, TensorDType
31
+ from .exceptions import (
32
+ ConnectionError,
33
+ KacheDBError,
34
+ PoolExhaustedError,
35
+ ProtocolError,
36
+ ResponseError,
37
+ TimeoutError,
38
+ )
39
+ from .pipeline import AsyncPipeline, Pipeline
40
+ from .pool import AsyncConnectionPool, ConnectionPool
41
+ from .tensor import attach_shm, detach_all, read_tensor, read_torch_tensor
42
+
43
+ __all__ = [
44
+ "TENSOR_DESCRIPTOR_MAGIC",
45
+ "AsyncConnectionPool",
46
+ "AsyncKacheClient",
47
+ "AsyncPipeline",
48
+ # Connection
49
+ "Connection",
50
+ "ConnectionError",
51
+ "ConnectionPool",
52
+ # Clients
53
+ "KacheClient",
54
+ # Exceptions
55
+ "KacheDBError",
56
+ # Pipeline
57
+ "Pipeline",
58
+ "PoolExhaustedError",
59
+ "ProtocolError",
60
+ "ResponseError",
61
+ # Tensor / Zero-Copy
62
+ "TensorBlockDescriptor",
63
+ "TensorDType",
64
+ "TimeoutError",
65
+ # Version
66
+ "__version__",
67
+ "attach_shm",
68
+ "detach_all",
69
+ "read_tensor",
70
+ "read_torch_tensor",
71
+ ]
kachedb/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Single-source version for kachedb Python SDK."""
2
+
3
+ __version__ = "0.1.0a2"
@@ -0,0 +1,169 @@
1
+ """
2
+ Async KacheDB client using ``asyncio``.
3
+
4
+ Provides the same high-level API as :class:`~kachedb.client.KacheClient`
5
+ but uses non-blocking ``asyncio`` streams for use in async ML inference
6
+ pipelines (vLLM, SGLang, etc.).
7
+
8
+ Usage::
9
+
10
+ import asyncio
11
+ from kachedb import AsyncKacheClient
12
+
13
+ async def main():
14
+ async with AsyncKacheClient() as client:
15
+ await client.set("user:1", "alice", ex=3600)
16
+ print(await client.get("user:1"))
17
+
18
+ asyncio.run(main())
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from .exceptions import ConnectionError
26
+ from .pipeline import AsyncPipeline
27
+ from .pool import AsyncConnectionPool
28
+ from .resp import AsyncRespReader, RespValue, encode_command
29
+
30
+ if TYPE_CHECKING:
31
+ import asyncio
32
+
33
+
34
+ class AsyncKacheClient:
35
+ """High-level async KacheDB client.
36
+
37
+ Parameters
38
+ ----------
39
+ host : str
40
+ Server hostname or IP address.
41
+ port : int
42
+ Server TCP port.
43
+ decode_responses : bool
44
+ If ``True``, decode byte responses to UTF-8 strings.
45
+ max_connections : int
46
+ Maximum async connection pool size.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ host: str = "127.0.0.1",
52
+ port: int = 6379,
53
+ *,
54
+ decode_responses: bool = False,
55
+ max_connections: int = 10,
56
+ ) -> None:
57
+ self.host = host
58
+ self.port = port
59
+ self.decode_responses = decode_responses
60
+ self._pool = AsyncConnectionPool(
61
+ host=host,
62
+ port=port,
63
+ max_connections=max_connections,
64
+ decode_responses=decode_responses,
65
+ )
66
+ self._reader: asyncio.StreamReader | None = None
67
+ self._writer: asyncio.StreamWriter | None = None
68
+ self._resp_reader: AsyncRespReader | None = None
69
+
70
+ async def connect(self) -> AsyncKacheClient:
71
+ """Establish a dedicated async connection."""
72
+ self._reader, self._writer, self._resp_reader = await self._pool.get_connection()
73
+ return self
74
+
75
+ async def close(self) -> None:
76
+ """Release the connection back to the pool."""
77
+ if self._writer is not None and self._reader is not None and self._resp_reader is not None:
78
+ await self._pool.release_connection(self._reader, self._writer, self._resp_reader)
79
+ self._reader = None
80
+ self._writer = None
81
+ self._resp_reader = None
82
+
83
+ async def disconnect_all(self) -> None:
84
+ """Close all pooled connections."""
85
+ await self._pool.disconnect_all()
86
+
87
+ async def __aenter__(self) -> AsyncKacheClient:
88
+ return await self.connect()
89
+
90
+ async def __aexit__(self, *args: Any) -> None:
91
+ await self.close()
92
+
93
+ # ── Internal Helpers ──────────────────────────────────────────────────
94
+
95
+ async def _execute(self, *args: str | bytes) -> RespValue:
96
+ """Execute a single command and return the response."""
97
+ if self._writer is None or self._resp_reader is None:
98
+ raise ConnectionError("Not connected to KacheDB")
99
+
100
+ data = encode_command(list(args))
101
+ self._writer.write(data)
102
+ await self._writer.drain()
103
+
104
+ response = await self._resp_reader.read_response()
105
+
106
+ if self.decode_responses and isinstance(response, bytes):
107
+ return response.decode("utf-8")
108
+
109
+ return response
110
+
111
+ # ── Redis-Compatible Commands ─────────────────────────────────────────
112
+
113
+ async def ping(self, message: str | None = None) -> str:
114
+ """Send ``PING`` and return ``PONG`` or the echoed message."""
115
+ args: list[str | bytes] = ["PING"]
116
+ if message is not None:
117
+ args.append(message)
118
+ result = await self._execute(*args)
119
+ return str(result) if result is not None else "PONG"
120
+
121
+ async def get(self, key: str | bytes) -> bytes | str | None:
122
+ """Retrieve the value for *key*."""
123
+ return await self._execute("GET", key) # type: ignore[return-value]
124
+
125
+ async def set(
126
+ self,
127
+ key: str | bytes,
128
+ value: str | bytes,
129
+ *,
130
+ ex: int | None = None,
131
+ px: int | None = None,
132
+ ) -> bool:
133
+ """Store *value* under *key* with an optional TTL."""
134
+ args: list[str | bytes] = ["SET", key, value]
135
+ if ex is not None:
136
+ args.extend(["EX", str(ex)])
137
+ elif px is not None:
138
+ args.extend(["PX", str(px)])
139
+ result = await self._execute(*args)
140
+ return result == "OK"
141
+
142
+ async def mget(self, *keys: str | bytes) -> list[bytes | str | None]:
143
+ """Batch-retrieve values for multiple keys."""
144
+ if not keys:
145
+ return []
146
+ result = await self._execute("MGET", *keys)
147
+ return result if isinstance(result, list) else [] # type: ignore[return-value]
148
+
149
+ async def delete(self, *keys: str | bytes) -> int:
150
+ """Delete one or more keys."""
151
+ if not keys:
152
+ return 0
153
+ result = await self._execute("DEL", *keys)
154
+ return int(result) if isinstance(result, int) else 0
155
+
156
+ async def exists(self, *keys: str | bytes) -> int:
157
+ """Check existence of one or more keys."""
158
+ if not keys:
159
+ return 0
160
+ result = await self._execute("EXISTS", *keys)
161
+ return int(result) if isinstance(result, int) else 0
162
+
163
+ # ── Pipeline ──────────────────────────────────────────────────────────
164
+
165
+ def pipeline(self) -> AsyncPipeline:
166
+ """Create an async pipeline for batching commands."""
167
+ if self._writer is None or self._resp_reader is None:
168
+ raise ConnectionError("Not connected to KacheDB")
169
+ return AsyncPipeline(self._writer, self._resp_reader)
kachedb/client.py ADDED
@@ -0,0 +1,216 @@
1
+ """
2
+ Synchronous KacheDB client.
3
+
4
+ Provides a high-level, Redis-like API for communicating with a KacheDB
5
+ daemon over TCP using the RESP2 wire protocol.
6
+
7
+ Usage::
8
+
9
+ from kachedb import KacheClient
10
+
11
+ with KacheClient() as client:
12
+ client.set("user:1", "alice", ex=3600)
13
+ print(client.get("user:1")) # b"alice"
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from .pipeline import Pipeline
21
+ from .pool import ConnectionPool
22
+
23
+ if TYPE_CHECKING:
24
+ from .connection import Connection
25
+ from .resp import RespValue
26
+
27
+
28
+ class KacheClient:
29
+ """High-level synchronous KacheDB client.
30
+
31
+ Supports both direct connection mode and connection pool mode.
32
+
33
+ Parameters
34
+ ----------
35
+ host : str
36
+ Server hostname or IP address.
37
+ port : int
38
+ Server TCP port.
39
+ socket_timeout : float | None
40
+ Socket timeout in seconds.
41
+ decode_responses : bool
42
+ If ``True``, decode byte responses to UTF-8 strings.
43
+ max_connections : int
44
+ Maximum pool size. Set to ``1`` to disable pooling.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ host: str = "127.0.0.1",
50
+ port: int = 6379,
51
+ *,
52
+ socket_timeout: float | None = 5.0,
53
+ decode_responses: bool = False,
54
+ max_connections: int = 10,
55
+ ) -> None:
56
+ self.host = host
57
+ self.port = port
58
+ self._pool = ConnectionPool(
59
+ host=host,
60
+ port=port,
61
+ max_connections=max_connections,
62
+ socket_timeout=socket_timeout,
63
+ decode_responses=decode_responses,
64
+ )
65
+ self._conn: Connection | None = None
66
+
67
+ def connect(self) -> KacheClient:
68
+ """Establish a dedicated connection (used with context manager)."""
69
+ self._conn = self._pool.get_connection()
70
+ return self
71
+
72
+ def close(self) -> None:
73
+ """Release the dedicated connection back to the pool."""
74
+ if self._conn is not None:
75
+ self._pool.release_connection(self._conn)
76
+ self._conn = None
77
+
78
+ def disconnect_all(self) -> None:
79
+ """Close all connections in the pool."""
80
+ self._pool.disconnect_all()
81
+
82
+ def __enter__(self) -> KacheClient:
83
+ return self.connect()
84
+
85
+ def __exit__(self, *args: Any) -> None:
86
+ self.close()
87
+
88
+ # ── Internal Helpers ──────────────────────────────────────────────────
89
+
90
+ def _get_conn(self) -> Connection:
91
+ """Get the active connection or checkout from pool."""
92
+ if self._conn is not None:
93
+ return self._conn
94
+ # For non-context-manager usage: get a connection per-call.
95
+ return self._pool.get_connection()
96
+
97
+ def _release_conn(self, conn: Connection) -> None:
98
+ """Release if it was a per-call checkout."""
99
+ if self._conn is None:
100
+ self._pool.release_connection(conn)
101
+
102
+ def _execute(self, *args: str | bytes) -> RespValue:
103
+ """Execute a single command and return the response."""
104
+ conn = self._get_conn()
105
+ try:
106
+ conn.send_command(*args)
107
+ return conn.read_response()
108
+ except Exception:
109
+ # On error, discard the connection.
110
+ conn.disconnect()
111
+ raise
112
+ finally:
113
+ self._release_conn(conn)
114
+
115
+ # ── Redis-Compatible Commands ─────────────────────────────────────────
116
+
117
+ def ping(self, message: str | None = None) -> str:
118
+ """Send ``PING`` and return ``PONG`` or the echoed message.
119
+
120
+ Parameters
121
+ ----------
122
+ message : str | None
123
+ Optional message to echo back.
124
+ """
125
+ args: list[str | bytes] = ["PING"]
126
+ if message is not None:
127
+ args.append(message)
128
+ result = self._execute(*args)
129
+ return str(result) if result is not None else "PONG"
130
+
131
+ def get(self, key: str | bytes) -> bytes | str | None:
132
+ """Retrieve the value for *key*.
133
+
134
+ Returns ``None`` if the key does not exist or has expired.
135
+ """
136
+ return self._execute("GET", key) # type: ignore[return-value]
137
+
138
+ def set(
139
+ self,
140
+ key: str | bytes,
141
+ value: str | bytes,
142
+ *,
143
+ ex: int | None = None,
144
+ px: int | None = None,
145
+ ) -> bool:
146
+ """Store *value* under *key* with an optional TTL.
147
+
148
+ Parameters
149
+ ----------
150
+ key : str | bytes
151
+ The cache key.
152
+ value : str | bytes
153
+ The value to store (binary-safe, up to 2 MB).
154
+ ex : int | None
155
+ Expiration time in **seconds**.
156
+ px : int | None
157
+ Expiration time in **milliseconds**.
158
+
159
+ Returns
160
+ -------
161
+ bool
162
+ ``True`` if the server acknowledged with ``OK``.
163
+ """
164
+ args: list[str | bytes] = ["SET", key, value]
165
+ if ex is not None:
166
+ args.extend(["EX", str(ex)])
167
+ elif px is not None:
168
+ args.extend(["PX", str(px)])
169
+ result = self._execute(*args)
170
+ return result == "OK"
171
+
172
+ def mget(self, *keys: str | bytes) -> list[bytes | str | None]:
173
+ """Batch-retrieve values for multiple keys in a single round-trip.
174
+
175
+ Returns a list of values (or ``None`` for missing/expired keys)
176
+ in the same order as the input keys.
177
+ """
178
+ if not keys:
179
+ return []
180
+ result = self._execute("MGET", *keys)
181
+ return result if isinstance(result, list) else [] # type: ignore[return-value]
182
+
183
+ def delete(self, *keys: str | bytes) -> int:
184
+ """Delete one or more keys.
185
+
186
+ Returns the number of keys that were actually removed.
187
+ """
188
+ if not keys:
189
+ return 0
190
+ result = self._execute("DEL", *keys)
191
+ return int(result) if isinstance(result, int) else 0
192
+
193
+ def exists(self, *keys: str | bytes) -> int:
194
+ """Check existence of one or more keys.
195
+
196
+ Returns the count of keys that exist and have not expired.
197
+ """
198
+ if not keys:
199
+ return 0
200
+ result = self._execute("EXISTS", *keys)
201
+ return int(result) if isinstance(result, int) else 0
202
+
203
+ # ── Pipeline ──────────────────────────────────────────────────────────
204
+
205
+ def pipeline(self) -> Pipeline:
206
+ """Create a pipeline for batching multiple commands.
207
+
208
+ Usage::
209
+
210
+ pipe = client.pipeline()
211
+ pipe.set("a", "1")
212
+ pipe.get("a")
213
+ results = pipe.execute()
214
+ """
215
+ conn = self._get_conn()
216
+ return Pipeline(conn)
kachedb/connection.py ADDED
@@ -0,0 +1,143 @@
1
+ """
2
+ Low-level TCP connection to a KacheDB server.
3
+
4
+ Manages the socket lifecycle, RESP encoding, and buffered response reading.
5
+ Used internally by :class:`~kachedb.client.KacheClient` and the connection pool.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import builtins
11
+ import contextlib
12
+ import socket
13
+
14
+ from .exceptions import ConnectionError, TimeoutError
15
+ from .resp import RespReader, RespValue, encode_command
16
+
17
+
18
+ class Connection:
19
+ """A single TCP connection to a KacheDB daemon.
20
+
21
+ Parameters
22
+ ----------
23
+ host : str
24
+ Server hostname or IP address.
25
+ port : int
26
+ Server TCP port.
27
+ socket_timeout : float | None
28
+ Socket timeout in seconds. ``None`` for blocking without timeout.
29
+ decode_responses : bool
30
+ If ``True``, decode byte responses to UTF-8 strings.
31
+ """
32
+
33
+ __slots__ = (
34
+ "_reader",
35
+ "_sock",
36
+ "decode_responses",
37
+ "host",
38
+ "port",
39
+ "socket_timeout",
40
+ )
41
+
42
+ def __init__(
43
+ self,
44
+ host: str = "127.0.0.1",
45
+ port: int = 6379,
46
+ *,
47
+ socket_timeout: float | None = 5.0,
48
+ decode_responses: bool = False,
49
+ ) -> None:
50
+ self.host = host
51
+ self.port = port
52
+ self.socket_timeout = socket_timeout
53
+ self.decode_responses = decode_responses
54
+ self._sock: socket.socket | None = None
55
+ self._reader: RespReader | None = None
56
+
57
+ @property
58
+ def is_connected(self) -> bool:
59
+ """Return ``True`` if the socket is open."""
60
+ return self._sock is not None
61
+
62
+ def connect(self) -> None:
63
+ """Establish TCP connection to the KacheDB server."""
64
+ if self._sock is not None:
65
+ return
66
+
67
+ try:
68
+ sock = socket.create_connection(
69
+ (self.host, self.port),
70
+ timeout=self.socket_timeout,
71
+ )
72
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
73
+ self._sock = sock
74
+ self._reader = RespReader(sock)
75
+ except OSError as exc:
76
+ raise ConnectionError(
77
+ f"Failed to connect to KacheDB at {self.host}:{self.port}: {exc}"
78
+ ) from exc
79
+
80
+ def disconnect(self) -> None:
81
+ """Close the TCP connection."""
82
+ if self._sock is not None:
83
+ with contextlib.suppress(OSError):
84
+ self._sock.close()
85
+ self._sock = None
86
+ self._reader = None
87
+
88
+ def send_command(self, *args: str | bytes) -> None:
89
+ """Encode and send a single RESP command."""
90
+ if self._sock is None:
91
+ raise ConnectionError("Not connected to KacheDB")
92
+
93
+ data = encode_command(list(args))
94
+ try:
95
+ self._sock.sendall(data)
96
+ except builtins.TimeoutError as exc:
97
+ self.disconnect()
98
+ raise TimeoutError(f"Timeout sending command to KacheDB: {exc}") from exc
99
+ except OSError as exc:
100
+ self.disconnect()
101
+ raise ConnectionError(f"Error sending command to KacheDB: {exc}") from exc
102
+
103
+ def send_packed(self, data: bytes) -> None:
104
+ """Send pre-encoded bytes (used by pipeline)."""
105
+ if self._sock is None:
106
+ raise ConnectionError("Not connected to KacheDB")
107
+
108
+ try:
109
+ self._sock.sendall(data)
110
+ except builtins.TimeoutError as exc:
111
+ self.disconnect()
112
+ raise TimeoutError(f"Timeout sending data to KacheDB: {exc}") from exc
113
+ except OSError as exc:
114
+ self.disconnect()
115
+ raise ConnectionError(f"Error sending data to KacheDB: {exc}") from exc
116
+
117
+ def read_response(self) -> RespValue:
118
+ """Read and decode one RESP response."""
119
+ if self._reader is None:
120
+ raise ConnectionError("Not connected to KacheDB")
121
+
122
+ try:
123
+ response = self._reader.read_response()
124
+ except builtins.TimeoutError as exc:
125
+ self.disconnect()
126
+ raise TimeoutError(f"Timeout reading response from KacheDB: {exc}") from exc
127
+ except OSError as exc:
128
+ self.disconnect()
129
+ raise ConnectionError(f"Error reading response from KacheDB: {exc}") from exc
130
+
131
+ if self.decode_responses and isinstance(response, bytes):
132
+ return response.decode("utf-8")
133
+
134
+ return response
135
+
136
+ def check_health(self) -> bool:
137
+ """Send a PING and verify PONG response. Returns ``False`` on any error."""
138
+ try:
139
+ self.send_command("PING")
140
+ response = self.read_response()
141
+ return response == "PONG"
142
+ except Exception:
143
+ return False