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 +133 -0
- prismdb-0.1.0/README.md +115 -0
- prismdb-0.1.0/prismdb/__init__.py +43 -0
- prismdb-0.1.0/prismdb/_codec.py +177 -0
- prismdb-0.1.0/prismdb/client.py +289 -0
- prismdb-0.1.0/prismdb/connection.py +116 -0
- prismdb-0.1.0/prismdb/document.py +50 -0
- prismdb-0.1.0/prismdb/errors.py +78 -0
- prismdb-0.1.0/prismdb/messages.py +354 -0
- prismdb-0.1.0/prismdb/py.typed +0 -0
- prismdb-0.1.0/prismdb/query.py +136 -0
- prismdb-0.1.0/prismdb/update.py +62 -0
- prismdb-0.1.0/prismdb/value.py +196 -0
- prismdb-0.1.0/prismdb.egg-info/PKG-INFO +133 -0
- prismdb-0.1.0/prismdb.egg-info/SOURCES.txt +18 -0
- prismdb-0.1.0/prismdb.egg-info/dependency_links.txt +1 -0
- prismdb-0.1.0/prismdb.egg-info/top_level.txt +1 -0
- prismdb-0.1.0/pyproject.toml +32 -0
- prismdb-0.1.0/setup.cfg +4 -0
- prismdb-0.1.0/tests/test_codec.py +160 -0
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.
|
prismdb-0.1.0/README.md
ADDED
|
@@ -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
|