shelfdb 3.0.1__tar.gz → 3.0.2__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.
- {shelfdb-3.0.1 → shelfdb-3.0.2}/PKG-INFO +32 -3
- {shelfdb-3.0.1 → shelfdb-3.0.2}/README.md +31 -2
- {shelfdb-3.0.1 → shelfdb-3.0.2}/pyproject.toml +3 -6
- {shelfdb-3.0.1 → shelfdb-3.0.2}/pyproject.toml.orig +3 -6
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/server.py +50 -7
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/session.py +20 -9
- shelfdb-3.0.2/src/shelfdb/protocol/write_admission.py +49 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/LICENSE +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/ai-skill/shelfdb-usage/SKILL.md +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/__main__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/cli.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/client/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/client/client.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/demo_poc.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/protocol.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/protocol/query_result.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/db.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/shelf/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/shelf/query.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/shelf/schema.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/shelf/shelf/shelf.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.0.2}/src/shelfdb/target.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shelfdb
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: A tiny database for Python
|
|
5
5
|
Author: Nitipit Nontasuwan
|
|
6
6
|
Author-email: Nitipit Nontasuwan <nitipit@gmail.com>
|
|
@@ -49,14 +49,28 @@ uv run python -m dev release-check
|
|
|
49
49
|
The gate audits locked dependencies, checks formatting, linting, types, supported
|
|
50
50
|
Python versions, strict documentation, release artifacts, metadata, and a clean
|
|
51
51
|
wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
|
|
52
|
-
and version tags.
|
|
52
|
+
and version tags. Before tagging, authenticated release maintainers also check
|
|
53
|
+
GitHub's repository alerts:
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
```bash
|
|
56
|
+
uv run python -m dev release-check --github
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Serve the docs locally (Zensical dev server; live reload is built in):
|
|
55
60
|
|
|
56
61
|
```bash
|
|
57
62
|
uv run python -m dev docs serve --port 9001 --livereload
|
|
58
63
|
```
|
|
59
64
|
|
|
65
|
+
`--livereload` is a legacy compatibility flag and is intentionally ignored by Zensical
|
|
66
|
+
(as Zensical has built-in live reload for docs serving).
|
|
67
|
+
|
|
68
|
+
Build docs for verification (Zensical build):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run python -m dev docs build
|
|
72
|
+
```
|
|
73
|
+
|
|
60
74
|
Publish the docs with mike to the `docs` branch:
|
|
61
75
|
|
|
62
76
|
```bash
|
|
@@ -101,6 +115,21 @@ from shelfdb.client import Client
|
|
|
101
115
|
client = await Client.connect("unix:///tmp/shelfdb.sock")
|
|
102
116
|
```
|
|
103
117
|
|
|
118
|
+
## Transaction behavior
|
|
119
|
+
|
|
120
|
+
- Readers use independent LMDB snapshots and do not join the writer queue.
|
|
121
|
+
- Remote write transactions queue, one writer at a time, across connections
|
|
122
|
+
served by the same `DB` object on one event loop. Independent local writers
|
|
123
|
+
and other server processes are outside this queue.
|
|
124
|
+
- A normal exit from `async with client.transaction(write=True)` commits;
|
|
125
|
+
an exception escaping the context rolls back the whole transaction.
|
|
126
|
+
Catching a query error inside the context leaves commit/rollback up to you.
|
|
127
|
+
- Keep transactions short: perform external HTTP/LLM calls before opening them.
|
|
128
|
+
Long-lived readers can delay page reuse; long-lived writers hold up the queue.
|
|
129
|
+
|
|
130
|
+
See [remote transaction usage](docs-src/usage/remote.md#concurrent-transactions)
|
|
131
|
+
for coordination limits and details on partial updates and error handling.
|
|
132
|
+
|
|
104
133
|
## Example
|
|
105
134
|
|
|
106
135
|
```python
|
|
@@ -30,14 +30,28 @@ uv run python -m dev release-check
|
|
|
30
30
|
The gate audits locked dependencies, checks formatting, linting, types, supported
|
|
31
31
|
Python versions, strict documentation, release artifacts, metadata, and a clean
|
|
32
32
|
wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
|
|
33
|
-
and version tags.
|
|
33
|
+
and version tags. Before tagging, authenticated release maintainers also check
|
|
34
|
+
GitHub's repository alerts:
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
```bash
|
|
37
|
+
uv run python -m dev release-check --github
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Serve the docs locally (Zensical dev server; live reload is built in):
|
|
36
41
|
|
|
37
42
|
```bash
|
|
38
43
|
uv run python -m dev docs serve --port 9001 --livereload
|
|
39
44
|
```
|
|
40
45
|
|
|
46
|
+
`--livereload` is a legacy compatibility flag and is intentionally ignored by Zensical
|
|
47
|
+
(as Zensical has built-in live reload for docs serving).
|
|
48
|
+
|
|
49
|
+
Build docs for verification (Zensical build):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run python -m dev docs build
|
|
53
|
+
```
|
|
54
|
+
|
|
41
55
|
Publish the docs with mike to the `docs` branch:
|
|
42
56
|
|
|
43
57
|
```bash
|
|
@@ -82,6 +96,21 @@ from shelfdb.client import Client
|
|
|
82
96
|
client = await Client.connect("unix:///tmp/shelfdb.sock")
|
|
83
97
|
```
|
|
84
98
|
|
|
99
|
+
## Transaction behavior
|
|
100
|
+
|
|
101
|
+
- Readers use independent LMDB snapshots and do not join the writer queue.
|
|
102
|
+
- Remote write transactions queue, one writer at a time, across connections
|
|
103
|
+
served by the same `DB` object on one event loop. Independent local writers
|
|
104
|
+
and other server processes are outside this queue.
|
|
105
|
+
- A normal exit from `async with client.transaction(write=True)` commits;
|
|
106
|
+
an exception escaping the context rolls back the whole transaction.
|
|
107
|
+
Catching a query error inside the context leaves commit/rollback up to you.
|
|
108
|
+
- Keep transactions short: perform external HTTP/LLM calls before opening them.
|
|
109
|
+
Long-lived readers can delay page reuse; long-lived writers hold up the queue.
|
|
110
|
+
|
|
111
|
+
See [remote transaction usage](docs-src/usage/remote.md#concurrent-transactions)
|
|
112
|
+
for coordination limits and details on partial updates and error handling.
|
|
113
|
+
|
|
85
114
|
## Example
|
|
86
115
|
|
|
87
116
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shelfdb"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.2"
|
|
8
8
|
description = "A tiny database for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -34,12 +34,9 @@ Repository = "https://github.com/keenlycode/shelfdb"
|
|
|
34
34
|
|
|
35
35
|
[dependency-groups]
|
|
36
36
|
dev = [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"mkdocs-shadcn>=0.10.4,<0.11",
|
|
40
|
-
"mkdocstrings[python]>=0.29.0,<0.30",
|
|
37
|
+
"zensical==0.0.52",
|
|
38
|
+
"mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac",
|
|
41
39
|
"pytest-cov>=7.0.0,<8",
|
|
42
|
-
"pymdown-extensions>=11.0.0,<12",
|
|
43
40
|
"pytest>=9.0.0,<10",
|
|
44
41
|
"ruff>=0.13.0,<0.14",
|
|
45
42
|
"tinydb>=4.8.2,<5",
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shelfdb"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.2"
|
|
8
8
|
description = "A tiny database for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -33,12 +33,9 @@ Repository = "https://github.com/keenlycode/shelfdb"
|
|
|
33
33
|
|
|
34
34
|
[dependency-groups]
|
|
35
35
|
dev = [
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"mkdocs-shadcn>=0.10.4,<0.11",
|
|
39
|
-
"mkdocstrings[python]>=0.29.0,<0.30",
|
|
36
|
+
"zensical==0.0.52",
|
|
37
|
+
"mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac",
|
|
40
38
|
"pytest-cov>=7.0.0,<8",
|
|
41
|
-
"pymdown-extensions>=11.0.0,<12",
|
|
42
39
|
"pytest>=9.0.0,<10",
|
|
43
40
|
"ruff>=0.13.0,<0.14",
|
|
44
41
|
"tinydb>=4.8.2,<5",
|
|
@@ -12,16 +12,39 @@ from shelfdb.shelf import DB
|
|
|
12
12
|
|
|
13
13
|
from .protocol import read_request, write_response
|
|
14
14
|
from .session import Session
|
|
15
|
+
from .write_admission import WriteLease
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ConnectionReader(asyncio.StreamReader):
|
|
19
|
+
"""Observe transport EOF/errors without a second consumer of request bytes."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.disconnected = asyncio.Event()
|
|
24
|
+
|
|
25
|
+
def feed_eof(self) -> None:
|
|
26
|
+
self.disconnected.set()
|
|
27
|
+
super().feed_eof()
|
|
28
|
+
|
|
29
|
+
def set_exception(self, exc) -> None:
|
|
30
|
+
self.disconnected.set()
|
|
31
|
+
super().set_exception(exc)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _protocol_factory(db: DB) -> asyncio.StreamReaderProtocol:
|
|
35
|
+
reader = _ConnectionReader()
|
|
36
|
+
return asyncio.StreamReaderProtocol(reader, partial(handle_client, db=db))
|
|
15
37
|
|
|
16
38
|
|
|
17
39
|
async def handle_client(
|
|
18
|
-
reader:
|
|
40
|
+
reader: _ConnectionReader,
|
|
19
41
|
writer: asyncio.StreamWriter,
|
|
20
42
|
*,
|
|
21
43
|
db: DB,
|
|
22
44
|
) -> None:
|
|
23
45
|
"""Serve one client connection with one session."""
|
|
24
46
|
session = Session(db)
|
|
47
|
+
lease = WriteLease(db)
|
|
25
48
|
|
|
26
49
|
try:
|
|
27
50
|
while True:
|
|
@@ -34,19 +57,35 @@ async def handle_client(
|
|
|
34
57
|
break
|
|
35
58
|
|
|
36
59
|
try:
|
|
60
|
+
if (
|
|
61
|
+
isinstance(command, dict)
|
|
62
|
+
and command.get("cmd") == "begin"
|
|
63
|
+
and command.get("mode") == "write"
|
|
64
|
+
and not session.active
|
|
65
|
+
):
|
|
66
|
+
if not await lease.acquire(reader.disconnected):
|
|
67
|
+
break
|
|
37
68
|
response = session.handle(command)
|
|
38
69
|
except Exception as exc:
|
|
39
70
|
response = {"ok": False, "error": str(exc)}
|
|
71
|
+
finally:
|
|
72
|
+
# A failed begin and any terminal transaction path give up admission.
|
|
73
|
+
# Query errors intentionally leave an active transaction usable.
|
|
74
|
+
if not session.active:
|
|
75
|
+
lease.release()
|
|
40
76
|
|
|
41
77
|
try:
|
|
42
78
|
await write_response(writer, response)
|
|
43
79
|
except Exception:
|
|
44
80
|
break
|
|
45
81
|
finally:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
try:
|
|
83
|
+
session.close()
|
|
84
|
+
finally:
|
|
85
|
+
lease.release()
|
|
86
|
+
writer.close()
|
|
87
|
+
with suppress(Exception):
|
|
88
|
+
await writer.wait_closed()
|
|
50
89
|
|
|
51
90
|
|
|
52
91
|
async def serve(
|
|
@@ -56,7 +95,9 @@ async def serve(
|
|
|
56
95
|
port: int = 0,
|
|
57
96
|
) -> asyncio.Server:
|
|
58
97
|
"""Start the minimal ShelfDB protocol server."""
|
|
59
|
-
return await asyncio.
|
|
98
|
+
return await asyncio.get_running_loop().create_server(
|
|
99
|
+
partial(_protocol_factory, db), host, port
|
|
100
|
+
)
|
|
60
101
|
|
|
61
102
|
|
|
62
103
|
class _UnixServer:
|
|
@@ -101,7 +142,9 @@ async def serve_unix(
|
|
|
101
142
|
socket_path = Path(path)
|
|
102
143
|
with suppress(FileNotFoundError):
|
|
103
144
|
socket_path.unlink()
|
|
104
|
-
server = await asyncio.
|
|
145
|
+
server = await asyncio.get_running_loop().create_unix_server(
|
|
146
|
+
partial(_protocol_factory, db), socket_path
|
|
147
|
+
)
|
|
105
148
|
return _UnixServer(server, socket_path)
|
|
106
149
|
|
|
107
150
|
|
|
@@ -94,8 +94,10 @@ class Session:
|
|
|
94
94
|
def close(self) -> None:
|
|
95
95
|
tx = self._tx
|
|
96
96
|
if tx is not None:
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
try:
|
|
98
|
+
tx.tx.abort()
|
|
99
|
+
finally:
|
|
100
|
+
self._clear_transaction()
|
|
99
101
|
|
|
100
102
|
def _begin(self, mode: str | None) -> dict[str, Any]:
|
|
101
103
|
if mode not in {"read", "write"}:
|
|
@@ -115,16 +117,25 @@ class Session:
|
|
|
115
117
|
|
|
116
118
|
def _commit(self) -> dict[str, Any]:
|
|
117
119
|
tx = self._require_tx()
|
|
118
|
-
|
|
119
|
-
tx.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
try:
|
|
121
|
+
if tx.is_write:
|
|
122
|
+
tx.commit()
|
|
123
|
+
else:
|
|
124
|
+
tx.tx.abort()
|
|
125
|
+
except Exception:
|
|
126
|
+
# LMDB may already have consumed the handle on commit failure.
|
|
127
|
+
# Best-effort abort also covers failures before commit reached LMDB.
|
|
128
|
+
try:
|
|
129
|
+
tx.tx.abort()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
raise
|
|
133
|
+
finally:
|
|
134
|
+
self._clear_transaction()
|
|
123
135
|
return _ok({"committed": True})
|
|
124
136
|
|
|
125
137
|
def _rollback(self) -> dict[str, Any]:
|
|
126
|
-
self.
|
|
127
|
-
self._clear_transaction()
|
|
138
|
+
self.close()
|
|
128
139
|
return _ok({"rolled_back": True})
|
|
129
140
|
|
|
130
141
|
def _query(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Event-loop-local writer admission, shared by listeners serving one DB.
|
|
2
|
+
|
|
3
|
+
This coordinates only server-side writers. It does not make synchronous LMDB
|
|
4
|
+
operations or independent local writers nonblocking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from weakref import WeakKeyDictionary
|
|
11
|
+
|
|
12
|
+
from shelfdb.shelf import DB
|
|
13
|
+
|
|
14
|
+
_GATES: WeakKeyDictionary[DB, asyncio.Lock] = WeakKeyDictionary()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WriteLease:
|
|
18
|
+
"""One connection's ownership of a DB's writer gate (not the transaction)."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, db: DB):
|
|
21
|
+
self._gate = _GATES.setdefault(db, asyncio.Lock())
|
|
22
|
+
self.held = False
|
|
23
|
+
|
|
24
|
+
async def acquire(self, disconnected: asyncio.Event) -> bool:
|
|
25
|
+
"""Wait without reading the stream; EOF/cancellation cannot leak a grant."""
|
|
26
|
+
waiting = asyncio.create_task(self._gate.acquire())
|
|
27
|
+
closed = asyncio.create_task(disconnected.wait())
|
|
28
|
+
try:
|
|
29
|
+
await asyncio.wait((waiting, closed), return_when=asyncio.FIRST_COMPLETED)
|
|
30
|
+
if disconnected.is_set():
|
|
31
|
+
return False
|
|
32
|
+
waiting.result()
|
|
33
|
+
self.held = True
|
|
34
|
+
return True
|
|
35
|
+
finally:
|
|
36
|
+
# No await between checking a grant and returning an abandoned one.
|
|
37
|
+
# A pending Lock.acquire handles its own cancellation safely.
|
|
38
|
+
if not self.held and waiting.done() and not waiting.cancelled():
|
|
39
|
+
if waiting.exception() is None and waiting.result():
|
|
40
|
+
self._gate.release()
|
|
41
|
+
waiting.cancel()
|
|
42
|
+
closed.cancel()
|
|
43
|
+
await asyncio.gather(waiting, closed, return_exceptions=True)
|
|
44
|
+
|
|
45
|
+
def release(self) -> None:
|
|
46
|
+
"""Release only this connection's grant, at most once."""
|
|
47
|
+
if self.held:
|
|
48
|
+
self.held = False
|
|
49
|
+
self._gate.release()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|