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.
- shelfdb-3.1.0/PKG-INFO +183 -0
- shelfdb-3.1.0/README.md +162 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/pyproject.toml +10 -11
- {shelfdb-3.0.1 → shelfdb-3.1.0}/pyproject.toml.orig +10 -9
- shelfdb-3.1.0/src/shelfdb/cli.py +53 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/client/client.py +1 -1
- shelfdb-3.1.0/src/shelfdb/protocol/__init__.py +59 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/protocol.py +4 -5
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/server.py +50 -7
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/session.py +20 -9
- shelfdb-3.1.0/src/shelfdb/protocol/write_admission.py +49 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/shelf/__init__.py +2 -1
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/shelf/db.py +2 -2
- {shelfdb-3.0.1/src/shelfdb/shelf → shelfdb-3.1.0/src/shelfdb}/shelf/query.py +1 -1
- shelfdb-3.0.1/PKG-INFO +0 -130
- shelfdb-3.0.1/README.md +0 -111
- shelfdb-3.0.1/ai-skill/shelfdb-usage/SKILL.md +0 -56
- shelfdb-3.0.1/src/shelfdb/cli.py +0 -109
- shelfdb-3.0.1/src/shelfdb/protocol/__init__.py +0 -35
- shelfdb-3.0.1/src/shelfdb/shelf/shelf/__init__.py +0 -4
- {shelfdb-3.0.1 → shelfdb-3.1.0}/LICENSE +0 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/__main__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/client/__init__.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/demo_poc.py +0 -0
- {shelfdb-3.0.1 → shelfdb-3.1.0}/src/shelfdb/protocol/query_result.py +0 -0
- {shelfdb-3.0.1/src/shelfdb/shelf → shelfdb-3.1.0/src/shelfdb}/shelf/schema.py +0 -0
- /shelfdb-3.0.1/src/shelfdb/shelf/shelf/shelf.py → /shelfdb-3.1.0/src/shelfdb/shelf/storage.py +0 -0
- {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
|
+
```
|
shelfdb-3.1.0/README.md
ADDED
|
@@ -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
|
|
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
|
-
"
|
|
38
|
-
"
|
|
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
|
|
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
|
-
"
|
|
37
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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()
|
|
@@ -9,8 +9,8 @@ from typing import Any
|
|
|
9
9
|
import lmdb
|
|
10
10
|
|
|
11
11
|
# lib: local
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
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 .
|
|
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/
|
shelfdb-3.0.1/src/shelfdb/cli.py
DELETED
|
@@ -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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/shelfdb-3.0.1/src/shelfdb/shelf/shelf/shelf.py → /shelfdb-3.1.0/src/shelfdb/shelf/storage.py
RENAMED
|
File without changes
|
|
File without changes
|