shelfdb 3.0.1__tar.gz → 3.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.
Files changed (29) hide show
  1. shelfdb-3.1.0/PKG-INFO +183 -0
  2. shelfdb-3.1.0/README.md +162 -0
  3. {shelfdb-3.0.1 → shelfdb-3.1.0}/pyproject.toml +10 -11
  4. {shelfdb-3.0.1 → shelfdb-3.1.0}/pyproject.toml.orig +10 -9
  5. shelfdb-3.1.0/src/shelfdb/cli.py +53 -0
  6. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/client/client.py +1 -1
  7. shelfdb-3.1.0/src/shelfdb/protocol/__init__.py +59 -0
  8. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/protocol.py +4 -5
  9. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/server.py +50 -7
  10. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/session.py +20 -9
  11. shelfdb-3.1.0/src/shelfdb/protocol/write_admission.py +49 -0
  12. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/shelf/__init__.py +2 -1
  13. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/shelf/db.py +2 -2
  14. {shelfdb-3.0.1/src/shelfdb/shelf → shelfdb-3.1.0/src/shelfdb}/shelf/query.py +1 -1
  15. shelfdb-3.0.1/PKG-INFO +0 -130
  16. shelfdb-3.0.1/README.md +0 -111
  17. shelfdb-3.0.1/ai-skill/shelfdb-usage/SKILL.md +0 -56
  18. shelfdb-3.0.1/src/shelfdb/cli.py +0 -109
  19. shelfdb-3.0.1/src/shelfdb/protocol/__init__.py +0 -35
  20. shelfdb-3.0.1/src/shelfdb/shelf/shelf/__init__.py +0 -4
  21. {shelfdb-3.0.1 → shelfdb-3.1.0}/LICENSE +0 -0
  22. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/__init__.py +0 -0
  23. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/__main__.py +0 -0
  24. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/client/__init__.py +0 -0
  25. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/demo_poc.py +0 -0
  26. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/query_result.py +0 -0
  27. {shelfdb-3.0.1/src/shelfdb/shelf → shelfdb-3.1.0/src/shelfdb}/shelf/schema.py +0 -0
  28. /shelfdb-3.0.1/src/shelfdb/shelf/shelf/shelf.py → /shelfdb-3.1.0/src/shelfdb/shelf/storage.py +0 -0
  29. {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/target.py +0 -0
shelfdb-3.1.0/PKG-INFO ADDED
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: shelfdb
3
+ Version: 3.1.0
4
+ Summary: A tiny database for Python
5
+ Author: Nitipit Nontasuwan
6
+ Author-email: Nitipit Nontasuwan <nitipit@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Dist: cyclopts>=4.10.2,<5
14
+ Requires-Dist: dill>=0.4.0,<0.5
15
+ Requires-Dist: lmdb>=1.7.3,<2
16
+ Requires-Dist: msgpack>=1.2.1,<2
17
+ Requires-Python: >=3.12, <3.15
18
+ Project-URL: Documentation, https://keenlycode.github.io/shelfdb/
19
+ Project-URL: Repository, https://github.com/keenlycode/shelfdb
20
+ Description-Content-Type: text/markdown
21
+
22
+ # shelfdb
23
+
24
+ Tiny LMDB-backed shelf database utilities.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install shelfdb
30
+ ```
31
+
32
+ > [!WARNING]
33
+ > The client/server protocol uses `dill` to support Python callables. Only use it
34
+ > between trusted processes; do not expose the server to untrusted clients or public
35
+ > networks.
36
+
37
+ ## Development
38
+
39
+ Python 3.12–3.14 is supported. Development uses Python 3.14, selected by
40
+ `.python-version`; compatibility checks cover Python 3.12 and 3.14. Python 3.15
41
+ is temporarily excluded because the released `dill` cannot serialize query
42
+ callbacks on it; support will be reassessed after an upstream fix is released.
43
+
44
+ Install development dependencies:
45
+
46
+ ```bash
47
+ uv sync --dev
48
+ ```
49
+
50
+ Development tooling lives in `src/dev/`, outside the published `shelfdb` package.
51
+ Use `--directory src` to run its module; workflow commands resolve paths from the
52
+ repository root. Documentation macros live in `src/macros.py`.
53
+
54
+ Unit tests mirror `src/` under `tests/unit/`: `shelfdb/`, `dev/`, and
55
+ `macros_test.py`. Integration tests cover cross-layer behavior; `tests/usage/`
56
+ contains executable usage examples.
57
+
58
+ Run the complete non-publishing release gate:
59
+
60
+ ```bash
61
+ uv run --directory src python -m dev release-check
62
+ ```
63
+
64
+ The gate audits locked dependencies, checks formatting, linting, types, supported
65
+ Python versions, strict documentation, release artifacts, metadata, and a clean
66
+ wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
67
+ and version tags. Before tagging, authenticated release maintainers also check
68
+ GitHub's repository alerts:
69
+
70
+ ```bash
71
+ uv run --directory src python -m dev release-check --github
72
+ ```
73
+
74
+ Serve the docs locally (Zensical dev server; live reload is built in):
75
+
76
+ ```bash
77
+ uv run --directory src python -m dev docs serve --port 9001 --livereload
78
+ ```
79
+
80
+ `--livereload` is a legacy compatibility flag and is intentionally ignored by Zensical
81
+ (as Zensical has built-in live reload for docs serving).
82
+
83
+ Build docs for verification (Zensical build):
84
+
85
+ ```bash
86
+ uv run --directory src python -m dev docs build
87
+ ```
88
+
89
+ Publish the docs with mike to the `docs` branch:
90
+
91
+ ```bash
92
+ uv run --directory src python -m dev docs publish
93
+ ```
94
+
95
+ Override the publish target when needed:
96
+
97
+ ```bash
98
+ uv run --directory src python -m dev docs publish --publish-version 3.0.1 --alias latest --branch docs --remote origin
99
+ ```
100
+
101
+ ## Server
102
+
103
+ Run the protocol server:
104
+
105
+ ```bash
106
+ shelfdb server
107
+ ```
108
+
109
+ Run the protocol server on a custom address:
110
+
111
+ ```bash
112
+ shelfdb server --url "tcp://0.0.0.0:17001" --db-path ./db
113
+ ```
114
+
115
+ ## Client
116
+
117
+ Connect a client:
118
+
119
+ ```python
120
+ from shelfdb.client import Client
121
+
122
+ client = await Client.connect("tcp://127.0.0.1:31337")
123
+ ```
124
+
125
+ Unix sockets also work:
126
+
127
+ ```python
128
+ from shelfdb.client import Client
129
+
130
+ client = await Client.connect("unix:///tmp/shelfdb.sock")
131
+ ```
132
+
133
+ ## Transaction behavior
134
+
135
+ - Readers use independent LMDB snapshots and do not join the writer queue.
136
+ - Remote write transactions queue, one writer at a time, across connections
137
+ served by the same `DB` object on one event loop. Independent local writers
138
+ and other server processes are outside this queue.
139
+ - A normal exit from `async with client.transaction(write=True)` commits;
140
+ an exception escaping the context rolls back the whole transaction.
141
+ Catching a query error inside the context leaves commit/rollback up to you.
142
+ - Keep transactions short: perform external HTTP/LLM calls before opening them.
143
+ Long-lived readers can delay page reuse; long-lived writers hold up the queue.
144
+
145
+ See [remote transaction usage](docs-src/usage/remote.md#concurrent-transactions)
146
+ for coordination limits and details on partial updates and error handling.
147
+
148
+ ## Large results
149
+
150
+ Remote messages are buffered, not streamed. The former 1 MiB cap is removed;
151
+ only the unsigned 32-bit payload length remains (4 GiB minus 1 byte). Large
152
+ queries can still exhaust memory, so paginate bulk reads with `.slice(...)`.
153
+ Store large assets externally and keep references in ShelfDB.
154
+
155
+ See [pagination and its limits](docs-src/usage/remote.md#large-results-and-pagination).
156
+
157
+ ## Example
158
+
159
+ ```python
160
+ from shelfdb.client import Client
161
+
162
+ client = await Client.connect("tcp://127.0.0.1:31337")
163
+
164
+ try:
165
+ async with client.transaction() as tx:
166
+ users = tx.shelf("users")
167
+
168
+ count = await users.count().query()
169
+ alice = await users.key("alice").item().query()
170
+ admins = await users.filter(
171
+ lambda item: item.value["role"] == "admin"
172
+ ).sort(reverse=True).query()
173
+
174
+ async with client.transaction(write=True) as tx:
175
+ users = tx.shelf("users")
176
+
177
+ await users.put("eve", {"role": "user"}).query()
178
+ await users.key("eve").update(
179
+ lambda item: {**item.value, "role": "admin"}
180
+ ).query()
181
+ finally:
182
+ await client.close()
183
+ ```
@@ -0,0 +1,162 @@
1
+ # shelfdb
2
+
3
+ Tiny LMDB-backed shelf database utilities.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install shelfdb
9
+ ```
10
+
11
+ > [!WARNING]
12
+ > The client/server protocol uses `dill` to support Python callables. Only use it
13
+ > between trusted processes; do not expose the server to untrusted clients or public
14
+ > networks.
15
+
16
+ ## Development
17
+
18
+ Python 3.12–3.14 is supported. Development uses Python 3.14, selected by
19
+ `.python-version`; compatibility checks cover Python 3.12 and 3.14. Python 3.15
20
+ is temporarily excluded because the released `dill` cannot serialize query
21
+ callbacks on it; support will be reassessed after an upstream fix is released.
22
+
23
+ Install development dependencies:
24
+
25
+ ```bash
26
+ uv sync --dev
27
+ ```
28
+
29
+ Development tooling lives in `src/dev/`, outside the published `shelfdb` package.
30
+ Use `--directory src` to run its module; workflow commands resolve paths from the
31
+ repository root. Documentation macros live in `src/macros.py`.
32
+
33
+ Unit tests mirror `src/` under `tests/unit/`: `shelfdb/`, `dev/`, and
34
+ `macros_test.py`. Integration tests cover cross-layer behavior; `tests/usage/`
35
+ contains executable usage examples.
36
+
37
+ Run the complete non-publishing release gate:
38
+
39
+ ```bash
40
+ uv run --directory src python -m dev release-check
41
+ ```
42
+
43
+ The gate audits locked dependencies, checks formatting, linting, types, supported
44
+ Python versions, strict documentation, release artifacts, metadata, and a clean
45
+ wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
46
+ and version tags. Before tagging, authenticated release maintainers also check
47
+ GitHub's repository alerts:
48
+
49
+ ```bash
50
+ uv run --directory src python -m dev release-check --github
51
+ ```
52
+
53
+ Serve the docs locally (Zensical dev server; live reload is built in):
54
+
55
+ ```bash
56
+ uv run --directory src python -m dev docs serve --port 9001 --livereload
57
+ ```
58
+
59
+ `--livereload` is a legacy compatibility flag and is intentionally ignored by Zensical
60
+ (as Zensical has built-in live reload for docs serving).
61
+
62
+ Build docs for verification (Zensical build):
63
+
64
+ ```bash
65
+ uv run --directory src python -m dev docs build
66
+ ```
67
+
68
+ Publish the docs with mike to the `docs` branch:
69
+
70
+ ```bash
71
+ uv run --directory src python -m dev docs publish
72
+ ```
73
+
74
+ Override the publish target when needed:
75
+
76
+ ```bash
77
+ uv run --directory src python -m dev docs publish --publish-version 3.0.1 --alias latest --branch docs --remote origin
78
+ ```
79
+
80
+ ## Server
81
+
82
+ Run the protocol server:
83
+
84
+ ```bash
85
+ shelfdb server
86
+ ```
87
+
88
+ Run the protocol server on a custom address:
89
+
90
+ ```bash
91
+ shelfdb server --url "tcp://0.0.0.0:17001" --db-path ./db
92
+ ```
93
+
94
+ ## Client
95
+
96
+ Connect a client:
97
+
98
+ ```python
99
+ from shelfdb.client import Client
100
+
101
+ client = await Client.connect("tcp://127.0.0.1:31337")
102
+ ```
103
+
104
+ Unix sockets also work:
105
+
106
+ ```python
107
+ from shelfdb.client import Client
108
+
109
+ client = await Client.connect("unix:///tmp/shelfdb.sock")
110
+ ```
111
+
112
+ ## Transaction behavior
113
+
114
+ - Readers use independent LMDB snapshots and do not join the writer queue.
115
+ - Remote write transactions queue, one writer at a time, across connections
116
+ served by the same `DB` object on one event loop. Independent local writers
117
+ and other server processes are outside this queue.
118
+ - A normal exit from `async with client.transaction(write=True)` commits;
119
+ an exception escaping the context rolls back the whole transaction.
120
+ Catching a query error inside the context leaves commit/rollback up to you.
121
+ - Keep transactions short: perform external HTTP/LLM calls before opening them.
122
+ Long-lived readers can delay page reuse; long-lived writers hold up the queue.
123
+
124
+ See [remote transaction usage](docs-src/usage/remote.md#concurrent-transactions)
125
+ for coordination limits and details on partial updates and error handling.
126
+
127
+ ## Large results
128
+
129
+ Remote messages are buffered, not streamed. The former 1 MiB cap is removed;
130
+ only the unsigned 32-bit payload length remains (4 GiB minus 1 byte). Large
131
+ queries can still exhaust memory, so paginate bulk reads with `.slice(...)`.
132
+ Store large assets externally and keep references in ShelfDB.
133
+
134
+ See [pagination and its limits](docs-src/usage/remote.md#large-results-and-pagination).
135
+
136
+ ## Example
137
+
138
+ ```python
139
+ from shelfdb.client import Client
140
+
141
+ client = await Client.connect("tcp://127.0.0.1:31337")
142
+
143
+ try:
144
+ async with client.transaction() as tx:
145
+ users = tx.shelf("users")
146
+
147
+ count = await users.count().query()
148
+ alice = await users.key("alice").item().query()
149
+ admins = await users.filter(
150
+ lambda item: item.value["role"] == "admin"
151
+ ).sort(reverse=True).query()
152
+
153
+ async with client.transaction(write=True) as tx:
154
+ users = tx.shelf("users")
155
+
156
+ await users.put("eve", {"role": "user"}).query()
157
+ await users.key("eve").update(
158
+ lambda item: {**item.value, "role": "admin"}
159
+ ).query()
160
+ finally:
161
+ await client.close()
162
+ ```
@@ -4,14 +4,16 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "shelfdb"
7
- version = "3.0.1"
7
+ version = "3.1.0"
8
8
  description = "A tiny database for Python"
9
9
  readme = "README.md"
10
- requires-python = ">=3.12"
10
+ requires-python = ">=3.12,<3.15"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
15
17
  "Operating System :: OS Independent",
16
18
  ]
17
19
  dependencies = [
@@ -34,22 +36,19 @@ Repository = "https://github.com/keenlycode/shelfdb"
34
36
 
35
37
  [dependency-groups]
36
38
  dev = [
37
- "mike>=2.1.3,<3",
38
- "mkdocs>=1.6.1,<2",
39
- "mkdocs-shadcn>=0.10.4,<0.11",
40
- "mkdocstrings[python]>=0.29.0,<0.30",
39
+ "zensical==0.0.52",
40
+ "mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac",
41
41
  "pytest-cov>=7.0.0,<8",
42
- "pymdown-extensions>=11.0.0,<12",
43
42
  "pytest>=9.0.0,<10",
44
43
  "ruff>=0.13.0,<0.14",
45
44
  "tinydb>=4.8.2,<5",
46
45
  "ty>=0.0.1a17,<0.0.2",
47
46
  ]
48
47
 
48
+ [tool.pytest.ini_options]
49
+ addopts = ["--import-mode=importlib"]
50
+ pythonpath = ["src"]
51
+
49
52
  [tool.uv.build-backend]
50
53
  module-name = "shelfdb"
51
54
  module-root = "src"
52
- source-include = ["ai-skill/**"]
53
-
54
- [tool.uv.build-backend.data]
55
- data = "ai-skill"
@@ -4,10 +4,10 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "shelfdb"
7
- version = "3.0.1"
7
+ version = "3.1.0"
8
8
  description = "A tiny database for Python"
9
9
  readme = "README.md"
10
- requires-python = ">=3.12"
10
+ requires-python = ">=3.12,<3.15"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [
@@ -15,6 +15,8 @@ authors = [
15
15
  ]
16
16
  classifiers = [
17
17
  "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
18
20
  "Operating System :: OS Independent",
19
21
  ]
20
22
  dependencies = [
@@ -33,20 +35,19 @@ Repository = "https://github.com/keenlycode/shelfdb"
33
35
 
34
36
  [dependency-groups]
35
37
  dev = [
36
- "mike>=2.1.3,<3",
37
- "mkdocs>=1.6.1,<2",
38
- "mkdocs-shadcn>=0.10.4,<0.11",
39
- "mkdocstrings[python]>=0.29.0,<0.30",
38
+ "zensical==0.0.52",
39
+ "mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac",
40
40
  "pytest-cov>=7.0.0,<8",
41
- "pymdown-extensions>=11.0.0,<12",
42
41
  "pytest>=9.0.0,<10",
43
42
  "ruff>=0.13.0,<0.14",
44
43
  "tinydb>=4.8.2,<5",
45
44
  "ty>=0.0.1a17,<0.0.2",
46
45
  ]
47
46
 
47
+ [tool.pytest.ini_options]
48
+ addopts = ["--import-mode=importlib"]
49
+ pythonpath = ["src"]
50
+
48
51
  [tool.uv.build-backend]
49
52
  module-name = "shelfdb"
50
53
  module-root = "src"
51
- data = { data = "ai-skill" }
52
- source-include = ["ai-skill/**"]
@@ -0,0 +1,53 @@
1
+ """Command-line interface for ShelfDB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import suppress
6
+ from pathlib import Path
7
+
8
+ from cyclopts import App
9
+
10
+ from shelfdb.protocol import serve, serve_unix
11
+ from shelfdb.shelf import DB
12
+ from shelfdb.target import parse_target, parse_tcp_location, parse_unix_location
13
+
14
+ app = App("shelfdb")
15
+
16
+
17
+ async def run_server(
18
+ *,
19
+ db_path: str,
20
+ url: str = "tcp://127.0.0.1:31337",
21
+ ) -> None:
22
+ scheme, location = parse_target(url)
23
+ socket_path = None
24
+
25
+ with DB(db_path) as db:
26
+ if scheme == "tcp":
27
+ host, port = parse_tcp_location(location)
28
+ server = await serve(db, host=host, port=port)
29
+ else:
30
+ socket_path = str(Path(parse_unix_location(location)))
31
+ server = await serve_unix(db, path=socket_path)
32
+
33
+ try:
34
+ await server.serve_forever()
35
+ finally:
36
+ server.close()
37
+ await server.wait_closed()
38
+ if socket_path is not None:
39
+ with suppress(FileNotFoundError):
40
+ Path(socket_path).unlink()
41
+
42
+
43
+ @app.command
44
+ async def server(
45
+ db_path: str = "db",
46
+ url: str = "tcp://127.0.0.1:31337",
47
+ ) -> None:
48
+ """Run the ShelfDB protocol server."""
49
+ await run_server(db_path=db_path, url=url)
50
+
51
+
52
+ def main(argv: list[str] | None = None) -> None:
53
+ app(argv, backend="asyncio", result_action="return_none")
@@ -6,7 +6,7 @@ from asyncio import StreamReader, StreamWriter, open_connection, open_unix_conne
6
6
  from contextlib import suppress
7
7
  from typing import Any
8
8
 
9
- from shelfdb.protocol import read_response, write_request
9
+ from shelfdb.protocol.protocol import read_response, write_request
10
10
  from shelfdb.protocol.query_result import denormalize_query_result
11
11
  from shelfdb.shelf import Item
12
12
  from shelfdb.target import parse_target, parse_tcp_location, parse_unix_location
@@ -0,0 +1,59 @@
1
+ """Wire helpers, with backward-compatible lazy server-side exports."""
2
+
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .protocol import (
7
+ MAX_FRAME_SIZE,
8
+ decode_request,
9
+ decode_response,
10
+ encode_request,
11
+ encode_response,
12
+ frame_payload,
13
+ read_payload,
14
+ read_request,
15
+ read_response,
16
+ write_payload,
17
+ write_request,
18
+ write_response,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from .server import handle_client, serve, serve_unix
23
+ from .session import Session
24
+
25
+
26
+ def __getattr__(name: str):
27
+ if name in {"handle_client", "serve", "serve_unix"}:
28
+ module = import_module(".server", __name__)
29
+ elif name == "Session":
30
+ module = import_module(".session", __name__)
31
+ else:
32
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
33
+ value = getattr(module, name)
34
+ globals()[name] = value
35
+ return value
36
+
37
+
38
+ def __dir__() -> list[str]:
39
+ return sorted(set(globals()) | set(__all__))
40
+
41
+
42
+ __all__ = [
43
+ "MAX_FRAME_SIZE",
44
+ "decode_request",
45
+ "decode_response",
46
+ "encode_request",
47
+ "encode_response",
48
+ "frame_payload",
49
+ "handle_client",
50
+ "read_payload",
51
+ "read_request",
52
+ "read_response",
53
+ "Session",
54
+ "serve",
55
+ "serve_unix",
56
+ "write_payload",
57
+ "write_request",
58
+ "write_response",
59
+ ]
@@ -10,7 +10,8 @@ import dill
10
10
  import msgpack
11
11
 
12
12
  _FRAME_HEADER: Final = struct.Struct(">I")
13
- MAX_FRAME_SIZE: Final = 1024 * 1024
13
+ # The unsigned 32-bit length header is the only frame-size limit.
14
+ MAX_FRAME_SIZE: Final = (1 << 32) - 1
14
15
 
15
16
 
16
17
  class WritableStream(Protocol):
@@ -20,9 +21,9 @@ class WritableStream(Protocol):
20
21
 
21
22
 
22
23
  def frame_payload(payload: bytes) -> bytes:
23
- """Prefix payload bytes with a 4-byte big-endian length."""
24
+ """Prefix a buffered payload with its unsigned 32-bit big-endian length."""
24
25
  if len(payload) > MAX_FRAME_SIZE:
25
- raise ValueError("payload exceeds max frame size")
26
+ raise ValueError("payload exceeds 32-bit frame length (4294967295 bytes)")
26
27
  return _FRAME_HEADER.pack(len(payload)) + payload
27
28
 
28
29
 
@@ -50,8 +51,6 @@ async def read_payload(reader: StreamReader) -> bytes:
50
51
  """Read one length-prefixed payload from a stream."""
51
52
  size_data = await reader.readexactly(_FRAME_HEADER.size)
52
53
  (size,) = _FRAME_HEADER.unpack(size_data)
53
- if size > MAX_FRAME_SIZE:
54
- raise ValueError("frame exceeds max frame size")
55
54
  return await reader.readexactly(size)
56
55
 
57
56
 
@@ -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: asyncio.StreamReader,
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
- session.close()
47
- writer.close()
48
- with suppress(Exception):
49
- await writer.wait_closed()
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.start_server(partial(handle_client, db=db), host, port)
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.start_unix_server(partial(handle_client, db=db), socket_path)
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
- tx.tx.abort()
98
- self._clear_transaction()
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
- if tx.is_write:
119
- tx.commit()
120
- else:
121
- tx.tx.abort()
122
- self._clear_transaction()
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._require_tx().tx.abort()
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()
@@ -1,4 +1,5 @@
1
1
  from .db import DB
2
- from .shelf import UNDEF, Item, MutationResult, ShelfQuery
2
+ from .query import ShelfQuery
3
+ from .schema import UNDEF, Item, MutationResult
3
4
 
4
5
  __all__ = ["DB", "UNDEF", "Item", "MutationResult", "ShelfQuery"]
@@ -9,8 +9,8 @@ from typing import Any
9
9
  import lmdb
10
10
 
11
11
  # lib: local
12
- from .shelf.query import ShelfQuery
13
- from .shelf.shelf import ShelfCursor, ShelfStore
12
+ from .query import ShelfQuery
13
+ from .storage import ShelfCursor, ShelfStore
14
14
 
15
15
  LmdbEnvironment = Any
16
16
  LmdbTransaction = Any
@@ -20,7 +20,7 @@ from itertools import islice
20
20
  from typing import Any
21
21
 
22
22
  from .schema import UNDEF, Item, MutationResult
23
- from .shelf import ShelfCursor, ShelfStore
23
+ from .storage import ShelfCursor, ShelfStore
24
24
 
25
25
  type Transform = Callable[[Iterator[Item]], Iterator[Item]]
26
26
 
shelfdb-3.0.1/PKG-INFO DELETED
@@ -1,130 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: shelfdb
3
- Version: 3.0.1
4
- Summary: A tiny database for Python
5
- Author: Nitipit Nontasuwan
6
- Author-email: Nitipit Nontasuwan <nitipit@gmail.com>
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Classifier: Programming Language :: Python :: 3.12
10
- Classifier: Operating System :: OS Independent
11
- Requires-Dist: cyclopts>=4.10.2,<5
12
- Requires-Dist: dill>=0.4.0,<0.5
13
- Requires-Dist: lmdb>=1.7.3,<2
14
- Requires-Dist: msgpack>=1.2.1,<2
15
- Requires-Python: >=3.12
16
- Project-URL: Documentation, https://keenlycode.github.io/shelfdb/
17
- Project-URL: Repository, https://github.com/keenlycode/shelfdb
18
- Description-Content-Type: text/markdown
19
-
20
- # shelfdb
21
-
22
- Tiny LMDB-backed shelf database utilities.
23
-
24
- ## Installation
25
-
26
- ```bash
27
- pip install shelfdb
28
- ```
29
-
30
- > [!WARNING]
31
- > The client/server protocol uses `dill` to support Python callables. Only use it
32
- > between trusted processes; do not expose the server to untrusted clients or public
33
- > networks.
34
-
35
- ## Development
36
-
37
- Install development dependencies:
38
-
39
- ```bash
40
- uv sync --dev
41
- ```
42
-
43
- Run the complete non-publishing release gate:
44
-
45
- ```bash
46
- uv run python -m dev release-check
47
- ```
48
-
49
- The gate audits locked dependencies, checks formatting, linting, types, supported
50
- Python versions, strict documentation, release artifacts, metadata, and a clean
51
- wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
52
- and version tags.
53
-
54
- Serve the docs locally:
55
-
56
- ```bash
57
- uv run python -m dev docs serve --port 9001 --livereload
58
- ```
59
-
60
- Publish the docs with mike to the `docs` branch:
61
-
62
- ```bash
63
- uv run python -m dev docs publish
64
- ```
65
-
66
- Override the publish target when needed:
67
-
68
- ```bash
69
- uv run python -m dev docs publish --publish-version 3.0.1 --alias latest --branch docs --remote origin
70
- ```
71
-
72
- ## Server
73
-
74
- Run the protocol server:
75
-
76
- ```bash
77
- shelfdb server
78
- ```
79
-
80
- Run the protocol server on a custom address:
81
-
82
- ```bash
83
- shelfdb server --url "tcp://0.0.0.0:17001" --db-path ./db
84
- ```
85
-
86
- ## Client
87
-
88
- Connect a client:
89
-
90
- ```python
91
- from shelfdb.client import Client
92
-
93
- client = await Client.connect("tcp://127.0.0.1:31337")
94
- ```
95
-
96
- Unix sockets also work:
97
-
98
- ```python
99
- from shelfdb.client import Client
100
-
101
- client = await Client.connect("unix:///tmp/shelfdb.sock")
102
- ```
103
-
104
- ## Example
105
-
106
- ```python
107
- from shelfdb.client import Client
108
-
109
- client = await Client.connect("tcp://127.0.0.1:31337")
110
-
111
- try:
112
- async with client.transaction() as tx:
113
- users = tx.shelf("users")
114
-
115
- count = await users.count().query()
116
- alice = await users.key("alice").item().query()
117
- admins = await users.filter(
118
- lambda item: item.value["role"] == "admin"
119
- ).sort(reverse=True).query()
120
-
121
- async with client.transaction(write=True) as tx:
122
- users = tx.shelf("users")
123
-
124
- await users.put("eve", {"role": "user"}).query()
125
- await users.key("eve").update(
126
- lambda item: {**item.value, "role": "admin"}
127
- ).query()
128
- finally:
129
- await client.close()
130
- ```
shelfdb-3.0.1/README.md DELETED
@@ -1,111 +0,0 @@
1
- # shelfdb
2
-
3
- Tiny LMDB-backed shelf database utilities.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- pip install shelfdb
9
- ```
10
-
11
- > [!WARNING]
12
- > The client/server protocol uses `dill` to support Python callables. Only use it
13
- > between trusted processes; do not expose the server to untrusted clients or public
14
- > networks.
15
-
16
- ## Development
17
-
18
- Install development dependencies:
19
-
20
- ```bash
21
- uv sync --dev
22
- ```
23
-
24
- Run the complete non-publishing release gate:
25
-
26
- ```bash
27
- uv run python -m dev release-check
28
- ```
29
-
30
- The gate audits locked dependencies, checks formatting, linting, types, supported
31
- Python versions, strict documentation, release artifacts, metadata, and a clean
32
- wheel installation. GitHub Actions runs the same gate on pull requests, `main`,
33
- and version tags.
34
-
35
- Serve the docs locally:
36
-
37
- ```bash
38
- uv run python -m dev docs serve --port 9001 --livereload
39
- ```
40
-
41
- Publish the docs with mike to the `docs` branch:
42
-
43
- ```bash
44
- uv run python -m dev docs publish
45
- ```
46
-
47
- Override the publish target when needed:
48
-
49
- ```bash
50
- uv run python -m dev docs publish --publish-version 3.0.1 --alias latest --branch docs --remote origin
51
- ```
52
-
53
- ## Server
54
-
55
- Run the protocol server:
56
-
57
- ```bash
58
- shelfdb server
59
- ```
60
-
61
- Run the protocol server on a custom address:
62
-
63
- ```bash
64
- shelfdb server --url "tcp://0.0.0.0:17001" --db-path ./db
65
- ```
66
-
67
- ## Client
68
-
69
- Connect a client:
70
-
71
- ```python
72
- from shelfdb.client import Client
73
-
74
- client = await Client.connect("tcp://127.0.0.1:31337")
75
- ```
76
-
77
- Unix sockets also work:
78
-
79
- ```python
80
- from shelfdb.client import Client
81
-
82
- client = await Client.connect("unix:///tmp/shelfdb.sock")
83
- ```
84
-
85
- ## Example
86
-
87
- ```python
88
- from shelfdb.client import Client
89
-
90
- client = await Client.connect("tcp://127.0.0.1:31337")
91
-
92
- try:
93
- async with client.transaction() as tx:
94
- users = tx.shelf("users")
95
-
96
- count = await users.count().query()
97
- alice = await users.key("alice").item().query()
98
- admins = await users.filter(
99
- lambda item: item.value["role"] == "admin"
100
- ).sort(reverse=True).query()
101
-
102
- async with client.transaction(write=True) as tx:
103
- users = tx.shelf("users")
104
-
105
- await users.put("eve", {"role": "user"}).query()
106
- await users.key("eve").update(
107
- lambda item: {**item.value, "role": "admin"}
108
- ).query()
109
- finally:
110
- await client.close()
111
- ```
@@ -1,56 +0,0 @@
1
- ---
2
- name: shelfdb-usage
3
- description: Use when writing, reviewing, or explaining ShelfDB local database code, async client/server code, transactions, or fluent queries.
4
- ---
5
-
6
- # ShelfDB usage
7
-
8
- Choose access mode from the deployment:
9
-
10
- - Use `shelfdb.shelf.DB` when the process can open the LMDB database directly.
11
- - Use `shelfdb.client.Client` when a separate trusted process runs `shelfdb server`.
12
-
13
- ## Safety boundary
14
-
15
- The remote protocol uses `dill` so queries can contain Python callables. Deserializing a
16
- request can execute arbitrary code in the server process. Never expose the server to
17
- untrusted clients or public networks.
18
-
19
- ## Local API
20
-
21
- Use synchronous transactions. Operations execute directly; do not add `.query()`.
22
-
23
- ```python
24
- from shelfdb.shelf import DB
25
-
26
- with DB("db") as db:
27
- with db.transaction(write=False) as tx:
28
- users = list(tx.shelf("users").items())
29
- ```
30
-
31
- ## Remote API
32
-
33
- Connect asynchronously, use read transactions by default, and pass `write=True` for
34
- mutations. Fluent operations only build a remote query; `await ...query()` sends it.
35
-
36
- ```python
37
- from shelfdb.client import Client
38
-
39
- client = await Client.connect("tcp://127.0.0.1:31337")
40
- try:
41
- async with client.transaction() as tx:
42
- users = await tx.shelf("users").items().query()
43
- finally:
44
- await client.close()
45
- ```
46
-
47
- ## Rules
48
-
49
- - Use `client.transaction()` for remote reads.
50
- - Use `client.transaction(write=True)` for remote mutations.
51
- - Always close a remote client with `await client.close()`.
52
- - Keep local and remote terminal behavior distinct.
53
- - In a ShelfDB source checkout, use `tests/usage/` as executable examples.
54
- - Consult the current documentation before assuming unsupported query behavior.
55
-
56
- Documentation: https://keenlycode.github.io/shelfdb/
@@ -1,109 +0,0 @@
1
- """Command-line interface for ShelfDB."""
2
-
3
- from __future__ import annotations
4
-
5
- import shutil
6
- import sysconfig
7
- from contextlib import suppress
8
- from pathlib import Path
9
-
10
- from cyclopts import App
11
-
12
- from shelfdb.protocol import serve, serve_unix
13
- from shelfdb.shelf import DB
14
- from shelfdb.target import parse_target, parse_tcp_location, parse_unix_location
15
-
16
- app = App("shelfdb")
17
- DEFAULT_AI_SKILL_INSTALL_PATH = ".agents/skills/shelfdb-usage"
18
-
19
-
20
- def repo_root() -> Path:
21
- return Path(__file__).resolve().parents[2]
22
-
23
-
24
- def bundled_ai_skill_path() -> Path:
25
- data_root = Path(sysconfig.get_path("data"))
26
- installed_path = data_root / "shelfdb-usage"
27
- if installed_path.exists():
28
- return installed_path
29
-
30
- repo_path = repo_root() / "ai-skill" / "shelfdb-usage"
31
- if repo_path.exists():
32
- return repo_path
33
-
34
- raise FileNotFoundError("Bundled ShelfDB AI skill not found.")
35
-
36
-
37
- def install_ai_skill(destination: Path, *, force: bool = False) -> Path:
38
- source_file = bundled_ai_skill_path() / "SKILL.md"
39
- if not source_file.is_file():
40
- raise FileNotFoundError("Bundled ShelfDB SKILL.md not found.")
41
-
42
- if destination.exists():
43
- if not destination.is_dir():
44
- raise NotADirectoryError(
45
- f"AI skill destination is not a directory: {destination}"
46
- )
47
- if not force:
48
- raise FileExistsError(
49
- f"AI skill destination already exists: {destination}. Use --force to replace SKILL.md."
50
- )
51
- else:
52
- destination.mkdir(parents=True)
53
-
54
- shutil.copy2(source_file, destination / "SKILL.md")
55
- return destination
56
-
57
-
58
- def prompt_ai_skill_install_path(
59
- default_path: str = DEFAULT_AI_SKILL_INSTALL_PATH,
60
- ) -> Path:
61
- response = input(f"Install ShelfDB AI skill to [{default_path}]: ").strip()
62
- return Path(response or default_path)
63
-
64
-
65
- async def run_server(
66
- *,
67
- db_path: str,
68
- url: str = "tcp://127.0.0.1:31337",
69
- ) -> None:
70
- scheme, location = parse_target(url)
71
- socket_path = None
72
-
73
- with DB(db_path) as db:
74
- if scheme == "tcp":
75
- host, port = parse_tcp_location(location)
76
- server = await serve(db, host=host, port=port)
77
- else:
78
- socket_path = str(Path(parse_unix_location(location)))
79
- server = await serve_unix(db, path=socket_path)
80
-
81
- try:
82
- await server.serve_forever()
83
- finally:
84
- server.close()
85
- await server.wait_closed()
86
- if socket_path is not None:
87
- with suppress(FileNotFoundError):
88
- Path(socket_path).unlink()
89
-
90
-
91
- @app.command
92
- async def server(
93
- db_path: str = "db",
94
- url: str = "tcp://127.0.0.1:31337",
95
- ) -> None:
96
- """Run the ShelfDB protocol server."""
97
- await run_server(db_path=db_path, url=url)
98
-
99
-
100
- @app.command(name="ai-skill-install")
101
- def ai_skill_install(path: str | None = None, force: bool = False) -> None:
102
- """Install the bundled ShelfDB AI skill to a local path."""
103
- destination = Path(path) if path is not None else prompt_ai_skill_install_path()
104
- installed = install_ai_skill(destination, force=force)
105
- print(f"Installed ShelfDB AI skill to {installed}")
106
-
107
-
108
- def main(argv: list[str] | None = None) -> None:
109
- app(argv, backend="asyncio", result_action="return_none")
@@ -1,35 +0,0 @@
1
- from .protocol import (
2
- MAX_FRAME_SIZE,
3
- decode_request,
4
- decode_response,
5
- encode_request,
6
- encode_response,
7
- frame_payload,
8
- read_payload,
9
- read_request,
10
- read_response,
11
- write_payload,
12
- write_request,
13
- write_response,
14
- )
15
- from .server import handle_client, serve, serve_unix
16
- from .session import Session
17
-
18
- __all__ = [
19
- "MAX_FRAME_SIZE",
20
- "decode_request",
21
- "decode_response",
22
- "encode_request",
23
- "encode_response",
24
- "frame_payload",
25
- "handle_client",
26
- "read_payload",
27
- "read_request",
28
- "read_response",
29
- "Session",
30
- "serve",
31
- "serve_unix",
32
- "write_payload",
33
- "write_request",
34
- "write_response",
35
- ]
@@ -1,4 +0,0 @@
1
- from .query import ShelfQuery
2
- from .schema import UNDEF, Item, MutationResult
3
-
4
- __all__ = ["UNDEF", "Item", "MutationResult", "ShelfQuery"]
File without changes
File without changes
File without changes
File without changes