prismdb 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.
prismdb-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: prismdb
3
+ Version: 0.1.0
4
+ Summary: Pure-Python client for PrismDB over the binary wire protocol (no native build).
5
+ Author: The Prism authors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/HafizMMoaz/prism-db/tree/main/sdks/python#readme
8
+ Project-URL: Repository, https://github.com/HafizMMoaz/prism-db
9
+ Project-URL: Issues, https://github.com/HafizMMoaz/prism-db/issues
10
+ Keywords: prismdb,prism,database,client,driver,sql,document,key-value,multi-model
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Database :: Front-Ends
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+
19
+ # prismdb (Python)
20
+
21
+ A **pure-Python** client for [PrismDB](https://github.com/HafizMMoaz/prism-db), speaking the binary
22
+ wire protocol directly over a TCP (or TLS) socket. No native extension, no build
23
+ toolchain — it runs anywhere CPython does.
24
+
25
+ > Implements `docs/specs/wire-protocol.md`. The byte layouts are kept in lockstep
26
+ > with the Rust `prism-protocol` crate and the reference Node SDK.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install prismdb
32
+ ```
33
+
34
+ Requires Python ≥ 3.8. No dependencies.
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from prismdb import Client, Q, U
40
+
41
+ db = Client.connect(host="127.0.0.1", port=4444, username="admin", password="admin")
42
+ with db:
43
+ # SQL
44
+ db.sql("CREATE TABLE users (id BIGINT PRIMARY KEY, name TEXT, age BIGINT)")
45
+ db.sql("INSERT INTO users VALUES (1,'alice',30),(2,'bob',25)")
46
+ res = db.sql("SELECT name, age FROM users WHERE age >= 30 ORDER BY age")
47
+ print(res.rows) # [{'name': 'alice', 'age': 30}]
48
+
49
+ # Key/value
50
+ db.kv.put("sessions", "sid-1", "payload")
51
+ v = db.kv.get("sessions", "sid-1") # bytes | None
52
+
53
+ # Documents, with query operators
54
+ db.doc.insert_one("people", {"name": "carol", "age": 41, "city": "NYC"})
55
+ adults = db.doc.find("people", Q.and_(Q.eq("city", "NYC"), Q.gt("age", 30)))
56
+
57
+ # A transaction is atomic across all three models
58
+ db.begin()
59
+ db.sql("INSERT INTO users VALUES (3,'dave',50)")
60
+ db.kv.put("sessions", "sid-2", "tx")
61
+ db.commit() # or db.abort()
62
+ ```
63
+
64
+ `Client` is a context manager; leaving the `with` block closes the connection.
65
+
66
+ ## API
67
+
68
+ ### `Client.connect(host="127.0.0.1", port=4444, *, username=None, password=None, database=None, tls=None, ...)`
69
+
70
+ Performs the `Hello`/`Auth` handshake. Omit `username` to skip authentication
71
+ (only useful against a server that doesn't require it). Pass `tls=True` (or an
72
+ `ssl.SSLContext`) for TLS. On a multi-database server, pass `database=` to select
73
+ it at connect; otherwise run `db.sql("USE <name>")` yourself.
74
+
75
+ ### SQL — `db.sql(text, params=None, *, return_rows=True)`
76
+
77
+ Returns a `SqlResult` with `.columns`, `.rows` (list of dicts keyed by column
78
+ name), `.raw` (cells in column order), and `.affected_rows` (int).
79
+
80
+ ### KV — `db.kv`
81
+
82
+ `get(ns, key) -> bytes | None`, `put(ns, key, value)`, `delete(ns, key)`. Keys
83
+ and values are `str` (UTF-8) or `bytes`.
84
+
85
+ ### Documents — `db.doc`
86
+
87
+ `insert_one` / `insert_many` (return the assigned `ObjectId`s), `find` / `find_one`,
88
+ `count`, `update_one` / `update_many`, `delete_one` / `delete_many`. Build filters
89
+ with `Q` and updates with `U`:
90
+
91
+ ```python
92
+ Q.all()
93
+ Q.eq("f", v); Q.ne; Q.gt; Q.lt; Q.gte; Q.lte
94
+ Q.in_("f", [a, b]); Q.nin("f", [a, b])
95
+ Q.exists("f", True)
96
+ Q.and_(a, b); Q.or_(a, b); Q.not_(a)
97
+
98
+ db.doc.update_one("people", Q.eq("name", "carol"), [
99
+ U.set("city", "Boston"),
100
+ U.inc("age", 1),
101
+ U.unset("temp"),
102
+ ])
103
+ ```
104
+
105
+ ### Transactions — `db.begin(mode="read_write")`, `db.commit(idempotency_key=0)`, `db.abort()`
106
+
107
+ One `Client` is one server session, so calls between `begin()` and `commit()`
108
+ run in that transaction. `commit(idempotency_key=...)` makes a retried commit safe.
109
+
110
+ ### Value mapping
111
+
112
+ Python → wire: `None`→Null, `bool`→Bool, `int`→Int64, `float`→Double,
113
+ `str`→Str, `bytes`→Binary, `datetime`→Timestamp, `ObjectId`→ObjectId. Use
114
+ `int32(n)`, `float64(n)`, `timestamp(us)` to force a type. On decode, Int64 and
115
+ Timestamp come back as `int`.
116
+
117
+ ## Develop
118
+
119
+ ```bash
120
+ python -m unittest discover -s tests # unit tests (no server needed)
121
+
122
+ # end-to-end against a running server:
123
+ prismd run ./data 127.0.0.1:4444
124
+ PRISM_HOST=127.0.0.1 PRISM_PORT=4444 python examples/quickstart.py
125
+ ```
126
+
127
+ ## Status / limitations
128
+
129
+ - Streamed (multi-frame) SQL/document results are not yet reassembled (the
130
+ current server replies in a single frame).
131
+ - KV `range`/`scan` are follow-ups.
132
+ - The client is synchronous; one `Client` owns one connection. Use a `Client`
133
+ per thread, or one per concurrent transaction.
@@ -0,0 +1,115 @@
1
+ # prismdb (Python)
2
+
3
+ A **pure-Python** client for [PrismDB](https://github.com/HafizMMoaz/prism-db), speaking the binary
4
+ wire protocol directly over a TCP (or TLS) socket. No native extension, no build
5
+ toolchain — it runs anywhere CPython does.
6
+
7
+ > Implements `docs/specs/wire-protocol.md`. The byte layouts are kept in lockstep
8
+ > with the Rust `prism-protocol` crate and the reference Node SDK.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install prismdb
14
+ ```
15
+
16
+ Requires Python ≥ 3.8. No dependencies.
17
+
18
+ ## Quick start
19
+
20
+ ```python
21
+ from prismdb import Client, Q, U
22
+
23
+ db = Client.connect(host="127.0.0.1", port=4444, username="admin", password="admin")
24
+ with db:
25
+ # SQL
26
+ db.sql("CREATE TABLE users (id BIGINT PRIMARY KEY, name TEXT, age BIGINT)")
27
+ db.sql("INSERT INTO users VALUES (1,'alice',30),(2,'bob',25)")
28
+ res = db.sql("SELECT name, age FROM users WHERE age >= 30 ORDER BY age")
29
+ print(res.rows) # [{'name': 'alice', 'age': 30}]
30
+
31
+ # Key/value
32
+ db.kv.put("sessions", "sid-1", "payload")
33
+ v = db.kv.get("sessions", "sid-1") # bytes | None
34
+
35
+ # Documents, with query operators
36
+ db.doc.insert_one("people", {"name": "carol", "age": 41, "city": "NYC"})
37
+ adults = db.doc.find("people", Q.and_(Q.eq("city", "NYC"), Q.gt("age", 30)))
38
+
39
+ # A transaction is atomic across all three models
40
+ db.begin()
41
+ db.sql("INSERT INTO users VALUES (3,'dave',50)")
42
+ db.kv.put("sessions", "sid-2", "tx")
43
+ db.commit() # or db.abort()
44
+ ```
45
+
46
+ `Client` is a context manager; leaving the `with` block closes the connection.
47
+
48
+ ## API
49
+
50
+ ### `Client.connect(host="127.0.0.1", port=4444, *, username=None, password=None, database=None, tls=None, ...)`
51
+
52
+ Performs the `Hello`/`Auth` handshake. Omit `username` to skip authentication
53
+ (only useful against a server that doesn't require it). Pass `tls=True` (or an
54
+ `ssl.SSLContext`) for TLS. On a multi-database server, pass `database=` to select
55
+ it at connect; otherwise run `db.sql("USE <name>")` yourself.
56
+
57
+ ### SQL — `db.sql(text, params=None, *, return_rows=True)`
58
+
59
+ Returns a `SqlResult` with `.columns`, `.rows` (list of dicts keyed by column
60
+ name), `.raw` (cells in column order), and `.affected_rows` (int).
61
+
62
+ ### KV — `db.kv`
63
+
64
+ `get(ns, key) -> bytes | None`, `put(ns, key, value)`, `delete(ns, key)`. Keys
65
+ and values are `str` (UTF-8) or `bytes`.
66
+
67
+ ### Documents — `db.doc`
68
+
69
+ `insert_one` / `insert_many` (return the assigned `ObjectId`s), `find` / `find_one`,
70
+ `count`, `update_one` / `update_many`, `delete_one` / `delete_many`. Build filters
71
+ with `Q` and updates with `U`:
72
+
73
+ ```python
74
+ Q.all()
75
+ Q.eq("f", v); Q.ne; Q.gt; Q.lt; Q.gte; Q.lte
76
+ Q.in_("f", [a, b]); Q.nin("f", [a, b])
77
+ Q.exists("f", True)
78
+ Q.and_(a, b); Q.or_(a, b); Q.not_(a)
79
+
80
+ db.doc.update_one("people", Q.eq("name", "carol"), [
81
+ U.set("city", "Boston"),
82
+ U.inc("age", 1),
83
+ U.unset("temp"),
84
+ ])
85
+ ```
86
+
87
+ ### Transactions — `db.begin(mode="read_write")`, `db.commit(idempotency_key=0)`, `db.abort()`
88
+
89
+ One `Client` is one server session, so calls between `begin()` and `commit()`
90
+ run in that transaction. `commit(idempotency_key=...)` makes a retried commit safe.
91
+
92
+ ### Value mapping
93
+
94
+ Python → wire: `None`→Null, `bool`→Bool, `int`→Int64, `float`→Double,
95
+ `str`→Str, `bytes`→Binary, `datetime`→Timestamp, `ObjectId`→ObjectId. Use
96
+ `int32(n)`, `float64(n)`, `timestamp(us)` to force a type. On decode, Int64 and
97
+ Timestamp come back as `int`.
98
+
99
+ ## Develop
100
+
101
+ ```bash
102
+ python -m unittest discover -s tests # unit tests (no server needed)
103
+
104
+ # end-to-end against a running server:
105
+ prismd run ./data 127.0.0.1:4444
106
+ PRISM_HOST=127.0.0.1 PRISM_PORT=4444 python examples/quickstart.py
107
+ ```
108
+
109
+ ## Status / limitations
110
+
111
+ - Streamed (multi-frame) SQL/document results are not yet reassembled (the
112
+ current server replies in a single frame).
113
+ - KV `range`/`scan` are follow-ups.
114
+ - The client is synchronous; one `Client` owns one connection. Use a `Client`
115
+ per thread, or one per concurrent transaction.
@@ -0,0 +1,43 @@
1
+ """prismdb — a pure-Python client for PrismDB over the binary wire protocol
2
+ (``docs/specs/wire-protocol.md``). No native build, no C extensions.
3
+
4
+ from prismdb import Client, Q, U
5
+
6
+ with Client.connect(host="127.0.0.1", port=4444, username="admin", password="admin") as db:
7
+ db.sql("CREATE TABLE users (id BIGINT PRIMARY KEY, name TEXT)")
8
+ db.sql("INSERT INTO users VALUES (1, 'alice')")
9
+ print(db.sql("SELECT * FROM users").rows)
10
+ """
11
+
12
+ from .client import Client, SqlResult
13
+ from .document import Document
14
+ from .errors import ErrorCode, ErrorInfo, PrismError, PrismServerError, ProtocolError
15
+ from .query import DocQuery, Q
16
+ from .update import DocUpdate, U
17
+ from .value import TAG, ObjectId, Typed, Value, float64, int32, int64, timestamp
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "Client",
23
+ "SqlResult",
24
+ "Document",
25
+ "Q",
26
+ "DocQuery",
27
+ "U",
28
+ "DocUpdate",
29
+ "ObjectId",
30
+ "Typed",
31
+ "Value",
32
+ "TAG",
33
+ "int32",
34
+ "int64",
35
+ "float64",
36
+ "timestamp",
37
+ "PrismError",
38
+ "PrismServerError",
39
+ "ProtocolError",
40
+ "ErrorInfo",
41
+ "ErrorCode",
42
+ "__version__",
43
+ ]
@@ -0,0 +1,177 @@
1
+ """Low-level binary codec: a growable little-endian ``Writer``, a bounds-checked
2
+ ``Reader``, and the length-prefixed frame helpers. The byte layouts mirror
3
+ ``crates/prism-protocol/src/codec.rs`` exactly (all multi-byte integers LE)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import struct
8
+
9
+ from .errors import ProtocolError
10
+
11
+ _U64_MASK = (1 << 64) - 1
12
+ _U128_MASK = (1 << 128) - 1
13
+
14
+
15
+ class Writer:
16
+ """A growable little-endian writer over a bytearray."""
17
+
18
+ __slots__ = ("_buf",)
19
+
20
+ def __init__(self) -> None:
21
+ self._buf = bytearray()
22
+
23
+ def u8(self, v: int) -> None:
24
+ self._buf.append(v & 0xFF)
25
+
26
+ def u16(self, v: int) -> None:
27
+ self._buf += struct.pack("<H", v & 0xFFFF)
28
+
29
+ def u32(self, v: int) -> None:
30
+ self._buf += struct.pack("<I", v & 0xFFFFFFFF)
31
+
32
+ def i32(self, v: int) -> None:
33
+ self._buf += struct.pack("<i", _to_signed(v, 32))
34
+
35
+ def u64(self, v: int) -> None:
36
+ self._buf += struct.pack("<Q", v & _U64_MASK)
37
+
38
+ def i64(self, v: int) -> None:
39
+ self._buf += struct.pack("<q", _to_signed(v, 64))
40
+
41
+ def f64(self, v: float) -> None:
42
+ self._buf += struct.pack("<d", v)
43
+
44
+ def u128(self, v: int) -> None:
45
+ """A 128-bit unsigned integer as 16 little-endian bytes."""
46
+ x = v & _U128_MASK
47
+ self._buf += struct.pack("<QQ", x & _U64_MASK, (x >> 64) & _U64_MASK)
48
+
49
+ def raw(self, b: bytes) -> None:
50
+ self._buf += b
51
+
52
+ def str_u16(self, s: str) -> None:
53
+ """A UTF-8 string with a u16 length prefix."""
54
+ b = s.encode("utf-8")
55
+ self.u16(len(b))
56
+ self._buf += b
57
+
58
+ def str_u32(self, s: str) -> None:
59
+ """A UTF-8 string with a u32 length prefix."""
60
+ b = s.encode("utf-8")
61
+ self.u32(len(b))
62
+ self._buf += b
63
+
64
+ def bytes_u16(self, b: bytes) -> None:
65
+ """A byte string with a u16 length prefix."""
66
+ self.u16(len(b))
67
+ self._buf += b
68
+
69
+ def bytes_u32(self, b: bytes) -> None:
70
+ """A byte string with a u32 length prefix."""
71
+ self.u32(len(b))
72
+ self._buf += b
73
+
74
+ def out(self) -> bytes:
75
+ return bytes(self._buf)
76
+
77
+
78
+ class Reader:
79
+ """A bounds-checked little-endian reader over a bytes-like object."""
80
+
81
+ __slots__ = ("_buf", "_p")
82
+
83
+ def __init__(self, buf: bytes) -> None:
84
+ self._buf = buf
85
+ self._p = 0
86
+
87
+ def _need(self, n: int) -> None:
88
+ if self._p + n > len(self._buf):
89
+ raise ProtocolError(f"truncated: need {n} bytes at offset {self._p}")
90
+
91
+ def u8(self) -> int:
92
+ self._need(1)
93
+ v = self._buf[self._p]
94
+ self._p += 1
95
+ return v
96
+
97
+ def u16(self) -> int:
98
+ self._need(2)
99
+ v = struct.unpack_from("<H", self._buf, self._p)[0]
100
+ self._p += 2
101
+ return v
102
+
103
+ def u32(self) -> int:
104
+ self._need(4)
105
+ v = struct.unpack_from("<I", self._buf, self._p)[0]
106
+ self._p += 4
107
+ return v
108
+
109
+ def i32(self) -> int:
110
+ self._need(4)
111
+ v = struct.unpack_from("<i", self._buf, self._p)[0]
112
+ self._p += 4
113
+ return v
114
+
115
+ def u64(self) -> int:
116
+ self._need(8)
117
+ v = struct.unpack_from("<Q", self._buf, self._p)[0]
118
+ self._p += 8
119
+ return v
120
+
121
+ def i64(self) -> int:
122
+ self._need(8)
123
+ v = struct.unpack_from("<q", self._buf, self._p)[0]
124
+ self._p += 8
125
+ return v
126
+
127
+ def f64(self) -> float:
128
+ self._need(8)
129
+ v = struct.unpack_from("<d", self._buf, self._p)[0]
130
+ self._p += 8
131
+ return v
132
+
133
+ def u128(self) -> int:
134
+ self._need(16)
135
+ lo, hi = struct.unpack_from("<QQ", self._buf, self._p)
136
+ self._p += 16
137
+ return (hi << 64) | lo
138
+
139
+ def raw(self, n: int) -> bytes:
140
+ self._need(n)
141
+ s = self._buf[self._p : self._p + n]
142
+ self._p += n
143
+ return bytes(s)
144
+
145
+ def str_u16(self) -> str:
146
+ return self.raw(self.u16()).decode("utf-8")
147
+
148
+ def str_u32(self) -> str:
149
+ return self.raw(self.u32()).decode("utf-8")
150
+
151
+ def bytes_u16(self) -> bytes:
152
+ return self.raw(self.u16())
153
+
154
+ def bytes_u32(self) -> bytes:
155
+ return self.raw(self.u32())
156
+
157
+ def remaining(self) -> int:
158
+ return len(self._buf) - self._p
159
+
160
+ def expect_end(self) -> None:
161
+ """Raise unless every byte has been consumed."""
162
+ if self.remaining() != 0:
163
+ raise ProtocolError(f"{self.remaining()} trailing byte(s) after message")
164
+
165
+
166
+ def _to_signed(v: int, bits: int) -> int:
167
+ """Wrap ``v`` into a signed ``bits``-wide integer (two's complement)."""
168
+ mask = (1 << bits) - 1
169
+ v &= mask
170
+ if v >= 1 << (bits - 1):
171
+ v -= 1 << bits
172
+ return v
173
+
174
+
175
+ def frame_encode(payload: bytes) -> bytes:
176
+ """Wrap a payload in a ``[len:u32][payload]`` frame."""
177
+ return struct.pack("<I", len(payload)) + payload