jsondocstore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mirko Ortensi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsondocstore
3
+ Version: 0.1.0
4
+ Summary: Simple JSON document storage on disk
5
+ Author-email: Mirko Ortensi <mirko.ortensi@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # JsonDocStore
13
+
14
+ Simple JSON document storage on disk.
15
+
16
+ Each document is stored as one file named `<key>.json`. The document key is the filename stem, not a field inside the JSON body.
17
+
18
+ If you want to organize documents into different folders, use one `JsonDocStore` instance per folder. Each instance manages only the JSON files in its own directory.
19
+
20
+ ## What It Does
21
+
22
+ - Stores one JSON document per file.
23
+ - Reads documents directly by filename.
24
+ - Uses optional in-memory indexes defined in `index.json`.
25
+ - Provides an interactive CLI in `cli.py`.
26
+
27
+ ## Data Model
28
+
29
+ - `get(key)` reads `<key>.json`.
30
+ - `insert(key, doc)` writes `<key>.json`.
31
+ - Valid keys may contain letters, digits, `.`, `_`, and `-`. No whitespace.
32
+
33
+ ## Schema
34
+
35
+ `index.json` is optional.
36
+
37
+ - `create=True` creates the directory if needed. It does not create `index.json`.
38
+ - `index_fields` defines which fields are indexed in memory.
39
+ - `get()`, `insert()`, `delete()`, and `list_all()` work without `index.json`.
40
+ - `query_by()` only works when an index exists. Querying without an index, or on a non-indexed field, raises an error.
41
+ - Indexes work only on top-level document fields, not nested paths.
42
+
43
+ Example:
44
+
45
+ ```json
46
+ {
47
+ "index_fields": ["email", "status"]
48
+ }
49
+ ```
50
+
51
+ For example, this works:
52
+
53
+ ```json
54
+ {
55
+ "email": "alice@example.com",
56
+ "profile": {
57
+ "city": "Rome"
58
+ }
59
+ }
60
+ ```
61
+
62
+ - you can index `email`
63
+ - you cannot index `profile.city`
64
+
65
+ ## CLI
66
+
67
+ Start the interactive shell:
68
+
69
+ ```bash
70
+ jsondocstore /path/to/store
71
+ ```
72
+
73
+ Commands:
74
+
75
+ - `list` prints document filenames only
76
+ - `listindexes` prints indexed fields, or `[]` if there is no `index.json`
77
+ - `queryby FIELD VALUE`
78
+ - `createindex FIELD`
79
+ - `deleteindex FIELD`
80
+ - `insert KEY JSON_DOCUMENT`
81
+ - `update KEY JSON_DOCUMENT`
82
+ - `delete KEY`
83
+ - `exit`
84
+
85
+ Example session:
86
+
87
+ ```text
88
+ $ jsondocstore ./data
89
+ jsondocstore> insert user-1 '{"username": "alice", "password": "secret1", "role": "admin"}'
90
+ jsondocstore> insert user-2 '{"username": "bob", "password": "secret2", "role": "user"}'
91
+ jsondocstore> insert user-3 '{"username": "carol", "password": "secret3", "role": "user"}'
92
+ jsondocstore> update user-2 '{"username": "bob", "password": "secret2", "role": "admin"}'
93
+ jsondocstore> list
94
+ [
95
+ "user-1.json",
96
+ "user-2.json",
97
+ "user-3.json"
98
+ ]
99
+ jsondocstore> createindex role
100
+ jsondocstore> queryby role user
101
+ [
102
+ {
103
+ "password": "secret2",
104
+ "role": "user",
105
+ "username": "bob"
106
+ },
107
+ {
108
+ "password": "secret3",
109
+ "role": "user",
110
+ "username": "carol"
111
+ }
112
+ ]
113
+ ```
114
+
115
+ ## Library
116
+
117
+ ```python
118
+ from jsondocstore import JsonDocStore
119
+
120
+ store = JsonDocStore("./data", create=True)
121
+ store.insert("user-1", {"username": "alice", "password": "secret1", "role": "admin"})
122
+ store.insert("user-2", {"username": "bob", "password": "secret2", "role": "user"})
123
+ store.insert("user-3", {"username": "carol", "password": "secret3", "role": "user"})
124
+ store.update("user-2", {"username": "bob", "password": "secret2", "role": "admin"})
125
+ names = store.list_all()
126
+ store.create_index("role")
127
+ admins = store.query_by("role", "admin")
128
+ doc = store.get("user-1")
129
+ ```
@@ -0,0 +1,118 @@
1
+ # JsonDocStore
2
+
3
+ Simple JSON document storage on disk.
4
+
5
+ Each document is stored as one file named `<key>.json`. The document key is the filename stem, not a field inside the JSON body.
6
+
7
+ If you want to organize documents into different folders, use one `JsonDocStore` instance per folder. Each instance manages only the JSON files in its own directory.
8
+
9
+ ## What It Does
10
+
11
+ - Stores one JSON document per file.
12
+ - Reads documents directly by filename.
13
+ - Uses optional in-memory indexes defined in `index.json`.
14
+ - Provides an interactive CLI in `cli.py`.
15
+
16
+ ## Data Model
17
+
18
+ - `get(key)` reads `<key>.json`.
19
+ - `insert(key, doc)` writes `<key>.json`.
20
+ - Valid keys may contain letters, digits, `.`, `_`, and `-`. No whitespace.
21
+
22
+ ## Schema
23
+
24
+ `index.json` is optional.
25
+
26
+ - `create=True` creates the directory if needed. It does not create `index.json`.
27
+ - `index_fields` defines which fields are indexed in memory.
28
+ - `get()`, `insert()`, `delete()`, and `list_all()` work without `index.json`.
29
+ - `query_by()` only works when an index exists. Querying without an index, or on a non-indexed field, raises an error.
30
+ - Indexes work only on top-level document fields, not nested paths.
31
+
32
+ Example:
33
+
34
+ ```json
35
+ {
36
+ "index_fields": ["email", "status"]
37
+ }
38
+ ```
39
+
40
+ For example, this works:
41
+
42
+ ```json
43
+ {
44
+ "email": "alice@example.com",
45
+ "profile": {
46
+ "city": "Rome"
47
+ }
48
+ }
49
+ ```
50
+
51
+ - you can index `email`
52
+ - you cannot index `profile.city`
53
+
54
+ ## CLI
55
+
56
+ Start the interactive shell:
57
+
58
+ ```bash
59
+ jsondocstore /path/to/store
60
+ ```
61
+
62
+ Commands:
63
+
64
+ - `list` prints document filenames only
65
+ - `listindexes` prints indexed fields, or `[]` if there is no `index.json`
66
+ - `queryby FIELD VALUE`
67
+ - `createindex FIELD`
68
+ - `deleteindex FIELD`
69
+ - `insert KEY JSON_DOCUMENT`
70
+ - `update KEY JSON_DOCUMENT`
71
+ - `delete KEY`
72
+ - `exit`
73
+
74
+ Example session:
75
+
76
+ ```text
77
+ $ jsondocstore ./data
78
+ jsondocstore> insert user-1 '{"username": "alice", "password": "secret1", "role": "admin"}'
79
+ jsondocstore> insert user-2 '{"username": "bob", "password": "secret2", "role": "user"}'
80
+ jsondocstore> insert user-3 '{"username": "carol", "password": "secret3", "role": "user"}'
81
+ jsondocstore> update user-2 '{"username": "bob", "password": "secret2", "role": "admin"}'
82
+ jsondocstore> list
83
+ [
84
+ "user-1.json",
85
+ "user-2.json",
86
+ "user-3.json"
87
+ ]
88
+ jsondocstore> createindex role
89
+ jsondocstore> queryby role user
90
+ [
91
+ {
92
+ "password": "secret2",
93
+ "role": "user",
94
+ "username": "bob"
95
+ },
96
+ {
97
+ "password": "secret3",
98
+ "role": "user",
99
+ "username": "carol"
100
+ }
101
+ ]
102
+ ```
103
+
104
+ ## Library
105
+
106
+ ```python
107
+ from jsondocstore import JsonDocStore
108
+
109
+ store = JsonDocStore("./data", create=True)
110
+ store.insert("user-1", {"username": "alice", "password": "secret1", "role": "admin"})
111
+ store.insert("user-2", {"username": "bob", "password": "secret2", "role": "user"})
112
+ store.insert("user-3", {"username": "carol", "password": "secret3", "role": "user"})
113
+ store.update("user-2", {"username": "bob", "password": "secret2", "role": "admin"})
114
+ names = store.list_all()
115
+ store.create_index("role")
116
+ admins = store.query_by("role", "admin")
117
+ doc = store.get("user-1")
118
+ ```
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jsondocstore"
7
+ version = "0.1.0"
8
+ description = "Simple JSON document storage on disk"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Mirko Ortensi", email = "mirko.ortensi@gmail.com" },
14
+ ]
15
+
16
+ [project.scripts]
17
+ jsondocstore = "jsondocstore.cli:main"
18
+
19
+ [tool.setuptools]
20
+ package-dir = {"" = "src"}
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .core import JsonDocStore
2
+
3
+ __all__ = ["JsonDocStore"]
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ import cmd
4
+ import json
5
+ import shlex
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ try:
10
+ import readline # noqa: F401
11
+ except ImportError:
12
+ readline = None
13
+
14
+ from .core import JsonDocStore, _json_dump
15
+
16
+
17
+ def _parse_query_value(text: str):
18
+ try:
19
+ return json.loads(text)
20
+ except json.JSONDecodeError:
21
+ return text
22
+
23
+
24
+ def _configure_readline() -> None:
25
+ if readline is None:
26
+ return
27
+ doc = getattr(readline, "__doc__", "") or ""
28
+ if "libedit" in doc:
29
+ readline.parse_and_bind("bind ^I rl_complete")
30
+ else:
31
+ readline.parse_and_bind("tab: complete")
32
+
33
+
34
+ class JsonDocStoreShell(cmd.Cmd):
35
+ intro = "JsonDocStore interactive shell. Type 'help' for commands."
36
+ prompt = "jsondocstore> "
37
+
38
+ def __init__(self, store: JsonDocStore):
39
+ super().__init__()
40
+ self.store = store
41
+
42
+ def get_names(self):
43
+ return [name for name in super().get_names() if name != "do_EOF"]
44
+
45
+ def emptyline(self):
46
+ return None
47
+
48
+ def _print_json(self, value):
49
+ print(_json_dump(value))
50
+
51
+ def _complete_from_options(self, text, options):
52
+ return sorted(option for option in options if option.startswith(text))
53
+
54
+ def _document_keys(self):
55
+ root = getattr(self.store, "root", None)
56
+ if root is None:
57
+ return []
58
+ return sorted(
59
+ path.stem
60
+ for path in Path(root).glob("*.json")
61
+ if path.name != "index.json"
62
+ )
63
+
64
+ def _document_fields(self):
65
+ fields = set()
66
+ root = getattr(self.store, "root", None)
67
+ if root is None:
68
+ return []
69
+ for path in Path(root).glob("*.json"):
70
+ if path.name == "index.json":
71
+ continue
72
+ try:
73
+ doc = json.loads(path.read_text(encoding="utf-8"))
74
+ fields.update(doc.keys())
75
+ except Exception:
76
+ continue
77
+ return sorted(field for field in fields if isinstance(field, str))
78
+
79
+ def _query_fields(self):
80
+ try:
81
+ indexes = self.store.list_indexes()
82
+ except Exception:
83
+ return []
84
+ return indexes or self._document_fields()
85
+
86
+ def complete_queryby(self, text, line, begidx, endidx):
87
+ args = shlex.split(line[:begidx])
88
+ if len(args) <= 1:
89
+ return self._complete_from_options(text, self._query_fields())
90
+ return []
91
+
92
+ def complete_createindex(self, text, line, begidx, endidx):
93
+ indexed = set(self.store.list_indexes())
94
+ fields = [field for field in self._document_fields() if field not in indexed]
95
+ return self._complete_from_options(text, fields)
96
+
97
+ def complete_deleteindex(self, text, line, begidx, endidx):
98
+ return self._complete_from_options(text, self.store.list_indexes())
99
+
100
+ def complete_delete(self, text, line, begidx, endidx):
101
+ return self._complete_from_options(text, self._document_keys())
102
+
103
+ def do_list(self, arg):
104
+ """List all documents."""
105
+ self._print_json(self.store.list_all())
106
+
107
+ def do_listindexes(self, arg):
108
+ """List indexed fields."""
109
+ self._print_json(self.store.list_indexes())
110
+
111
+ def do_queryby(self, arg):
112
+ """queryby FIELD VALUE"""
113
+ try:
114
+ field, value = shlex.split(arg)
115
+ except ValueError:
116
+ print("Usage: queryby FIELD VALUE")
117
+ return
118
+
119
+ try:
120
+ self._print_json(self.store.query_by(field, _parse_query_value(value)))
121
+ except Exception as e:
122
+ print(f"Error: {e}")
123
+
124
+ def do_createindex(self, arg):
125
+ """createindex FIELD"""
126
+ field = arg.strip()
127
+ if not field:
128
+ print("Usage: createindex FIELD")
129
+ return
130
+
131
+ try:
132
+ self.store.create_index(field)
133
+ print(f"Created index {field}")
134
+ except Exception as e:
135
+ print(f"Error: {e}")
136
+
137
+ def do_deleteindex(self, arg):
138
+ """deleteindex FIELD"""
139
+ field = arg.strip()
140
+ if not field:
141
+ print("Usage: deleteindex FIELD")
142
+ return
143
+
144
+ try:
145
+ deleted = self.store.delete_index(field)
146
+ if deleted:
147
+ print(f"Deleted index {field}")
148
+ else:
149
+ print(f"Index not found: {field}")
150
+ except Exception as e:
151
+ print(f"Error: {e}")
152
+
153
+ def do_insert(self, arg):
154
+ """insert KEY JSON_DOCUMENT"""
155
+ if not arg.strip():
156
+ print("Usage: insert KEY JSON_DOCUMENT")
157
+ return
158
+
159
+ try:
160
+ parts = shlex.split(arg)
161
+ if len(parts) < 2:
162
+ raise ValueError
163
+ key = parts[0]
164
+ json_text = " ".join(parts[1:])
165
+ except ValueError:
166
+ print("Usage: insert KEY JSON_DOCUMENT")
167
+ return
168
+
169
+ try:
170
+ self._print_json(self.store.insert(key, json.loads(json_text)))
171
+ except Exception as e:
172
+ print(f"Error: {e}")
173
+
174
+ def do_update(self, arg):
175
+ """update KEY JSON_DOCUMENT"""
176
+ if not arg.strip():
177
+ print("Usage: update KEY JSON_DOCUMENT")
178
+ return
179
+
180
+ try:
181
+ parts = shlex.split(arg)
182
+ if len(parts) < 2:
183
+ raise ValueError
184
+ key = parts[0]
185
+ json_text = " ".join(parts[1:])
186
+ except ValueError:
187
+ print("Usage: update KEY JSON_DOCUMENT")
188
+ return
189
+
190
+ try:
191
+ self._print_json(self.store.update(key, json.loads(json_text)))
192
+ except Exception as e:
193
+ print(f"Error: {e}")
194
+
195
+ def do_delete(self, arg):
196
+ """delete PK"""
197
+ pk = arg.strip()
198
+ if not pk:
199
+ print("Usage: delete PK")
200
+ return
201
+
202
+ try:
203
+ self.store.delete(pk)
204
+ print(f"Deleted {pk}")
205
+ except Exception as e:
206
+ print(f"Error: {e}")
207
+
208
+ def do_exit(self, arg):
209
+ """Exit the shell."""
210
+ return True
211
+
212
+ def do_EOF(self, arg):
213
+ """Exit on Ctrl-D."""
214
+ print()
215
+ return True
216
+
217
+
218
+ def main():
219
+ if len(sys.argv) != 2:
220
+ print("Usage: python -m jsondocstore /path/to/db", file=sys.stderr)
221
+ return 1
222
+
223
+ _configure_readline()
224
+ root = Path(sys.argv[1])
225
+ try:
226
+ store = JsonDocStore(root, create=True)
227
+ except Exception as e:
228
+ print(f"Error: {e}", file=sys.stderr)
229
+ return 1
230
+
231
+ try:
232
+ JsonDocStoreShell(store).cmdloop()
233
+ except KeyboardInterrupt:
234
+ print()
235
+ return 0
236
+
237
+
238
+ if __name__ == "__main__":
239
+ raise SystemExit(main())
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+ import json
6
+ import re
7
+ import tempfile
8
+ from typing import Any
9
+
10
+
11
+ def _json_dump(value: Any) -> str:
12
+ return json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
13
+
14
+
15
+ _VALID_KEY_RE = re.compile(r"^[A-Za-z0-9._-]+$")
16
+
17
+
18
+ class JsonDocStore:
19
+ def __init__(self, root: str | Path, create: bool = False):
20
+ self.root = Path(root)
21
+ if create:
22
+ self.root.mkdir(parents=True, exist_ok=True)
23
+ elif not self.root.exists():
24
+ raise ValueError(f"Directory does not exist: {self.root}")
25
+ if not self.root.is_dir():
26
+ raise ValueError(f"Path is not a directory: {self.root}")
27
+
28
+ self.schema_path = self.root / "index.json"
29
+ self.schema = self._load_schema() if self.schema_path.exists() else None
30
+ self.index_fields = list(self.schema.get("index_fields", [])) if self.schema else []
31
+ self.indexes: dict[str, dict[Any, set[str]]] = {field: defaultdict(set) for field in self.index_fields}
32
+ if self.schema is not None:
33
+ self._rebuild_index()
34
+
35
+ def _load_schema(self) -> dict[str, Any]:
36
+ schema = json.loads(self.schema_path.read_text(encoding="utf-8"))
37
+ if "index_fields" not in schema:
38
+ raise ValueError("index.json must contain 'index_fields'")
39
+ if not isinstance(schema["index_fields"], list):
40
+ raise ValueError("'index_fields' must be a list")
41
+ if not all(isinstance(field, str) for field in schema["index_fields"]):
42
+ raise ValueError("'index_fields' entries must be strings")
43
+ return schema
44
+
45
+ def _doc_path(self, pk: str) -> Path:
46
+ return self.root / f"{pk}.json"
47
+
48
+ def _validate_key(self, pk: str) -> None:
49
+ if not pk:
50
+ raise ValueError("Document key must not be empty")
51
+ if pk in {".", ".."}:
52
+ raise ValueError(f"Invalid document key: {pk}")
53
+ if pk == "index":
54
+ raise ValueError("Document key 'index' is reserved")
55
+ if not _VALID_KEY_RE.fullmatch(pk):
56
+ raise ValueError(
57
+ "Invalid document key. Use only letters, digits, dot, underscore, or hyphen"
58
+ )
59
+
60
+ def _rebuild_index(self) -> None:
61
+ for field in self.index_fields:
62
+ self.indexes[field].clear()
63
+ for path in sorted(self.root.glob("*.json")):
64
+ if path.name == "index.json":
65
+ continue
66
+ doc = json.loads(path.read_text(encoding="utf-8"))
67
+ self._add_indexes(path.stem, doc)
68
+
69
+ def _list_doc_names(self) -> list[str]:
70
+ return [
71
+ path.name
72
+ for path in sorted(self.root.glob("*.json"))
73
+ if path.name != "index.json"
74
+ ]
75
+
76
+ def _add_indexes(self, pk: str, doc: dict[str, Any]) -> None:
77
+ for field in self.index_fields:
78
+ if field in doc:
79
+ self.indexes[field][doc[field]].add(pk)
80
+
81
+ def _remove_indexes(self, pk: str, doc: dict[str, Any]) -> None:
82
+ for field in self.index_fields:
83
+ if field in doc:
84
+ value = doc[field]
85
+ bucket = self.indexes[field].get(value)
86
+ if bucket is not None:
87
+ bucket.discard(pk)
88
+ if not bucket:
89
+ del self.indexes[field][value]
90
+
91
+ def _write_json_atomic(self, path: Path, obj: dict[str, Any]) -> None:
92
+ with tempfile.NamedTemporaryFile(
93
+ "w", encoding="utf-8", dir=str(self.root), delete=False
94
+ ) as tmp:
95
+ tmp.write(_json_dump(obj))
96
+ tmp_path = Path(tmp.name)
97
+ tmp_path.replace(path)
98
+
99
+ def list_all(self) -> list[str]:
100
+ return self._list_doc_names()
101
+
102
+ def get(self, pk: str) -> dict[str, Any]:
103
+ self._validate_key(pk)
104
+ path = self._doc_path(pk)
105
+ if not path.exists():
106
+ raise KeyError(f"Document not found: {pk}")
107
+ return json.loads(path.read_text(encoding="utf-8"))
108
+
109
+ def query_by(self, field: str, value: Any) -> list[dict[str, Any]]:
110
+ if self.schema is None:
111
+ raise ValueError("Cannot query without an index. Create an index first")
112
+ if field not in self.indexes:
113
+ raise ValueError(f"Field is not indexed: {field}")
114
+ return [self.get(pk) for pk in sorted(self.indexes[field].get(value, ()))]
115
+
116
+ def create_index(self, field: str) -> None:
117
+ if self.schema is None:
118
+ self.schema = {"index_fields": []}
119
+ self.index_fields = []
120
+ self.indexes = {}
121
+ if field in self.indexes:
122
+ raise ValueError(f"Index already exists: {field}. Delete it before recreating")
123
+ self.index_fields.append(field)
124
+ self.indexes[field] = defaultdict(set)
125
+ self.schema["index_fields"] = list(self.index_fields)
126
+ self._write_json_atomic(self.schema_path, self.schema)
127
+ self._rebuild_index()
128
+
129
+ def list_indexes(self) -> list[str]:
130
+ return sorted(self.index_fields)
131
+
132
+ def delete_index(self, field: str) -> bool:
133
+ if self.schema is None:
134
+ raise ValueError("Cannot delete an index without an index.json")
135
+ if field not in self.indexes:
136
+ return False
137
+ self.index_fields = [name for name in self.index_fields if name != field]
138
+ del self.indexes[field]
139
+ self.schema["index_fields"] = list(self.index_fields)
140
+ self._write_json_atomic(self.schema_path, self.schema)
141
+ return True
142
+
143
+ def insert(self, pk: str, doc: dict[str, Any]) -> dict[str, Any]:
144
+ self._validate_key(pk)
145
+ path = self._doc_path(pk)
146
+ if path.exists():
147
+ raise ValueError(f"Primary key already exists: {pk}")
148
+ if self.schema is not None:
149
+ self._add_indexes(pk, doc)
150
+ self._write_json_atomic(path, doc)
151
+ return doc
152
+
153
+ def update(self, pk: str, doc: dict[str, Any]) -> dict[str, Any]:
154
+ self._validate_key(pk)
155
+ path = self._doc_path(pk)
156
+ if not path.exists():
157
+ raise KeyError(f"Document not found: {pk}")
158
+ if self.schema is not None:
159
+ old_doc = self.get(pk)
160
+ self._remove_indexes(pk, old_doc)
161
+ self._add_indexes(pk, doc)
162
+ self._write_json_atomic(path, doc)
163
+ return doc
164
+
165
+ def delete(self, pk: str) -> None:
166
+ self._validate_key(pk)
167
+ path = self._doc_path(pk)
168
+ if not path.exists():
169
+ raise KeyError(f"Document not found: {pk}")
170
+ if self.schema is not None:
171
+ doc = self.get(pk)
172
+ self._remove_indexes(pk, doc)
173
+ path.unlink()
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsondocstore
3
+ Version: 0.1.0
4
+ Summary: Simple JSON document storage on disk
5
+ Author-email: Mirko Ortensi <mirko.ortensi@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # JsonDocStore
13
+
14
+ Simple JSON document storage on disk.
15
+
16
+ Each document is stored as one file named `<key>.json`. The document key is the filename stem, not a field inside the JSON body.
17
+
18
+ If you want to organize documents into different folders, use one `JsonDocStore` instance per folder. Each instance manages only the JSON files in its own directory.
19
+
20
+ ## What It Does
21
+
22
+ - Stores one JSON document per file.
23
+ - Reads documents directly by filename.
24
+ - Uses optional in-memory indexes defined in `index.json`.
25
+ - Provides an interactive CLI in `cli.py`.
26
+
27
+ ## Data Model
28
+
29
+ - `get(key)` reads `<key>.json`.
30
+ - `insert(key, doc)` writes `<key>.json`.
31
+ - Valid keys may contain letters, digits, `.`, `_`, and `-`. No whitespace.
32
+
33
+ ## Schema
34
+
35
+ `index.json` is optional.
36
+
37
+ - `create=True` creates the directory if needed. It does not create `index.json`.
38
+ - `index_fields` defines which fields are indexed in memory.
39
+ - `get()`, `insert()`, `delete()`, and `list_all()` work without `index.json`.
40
+ - `query_by()` only works when an index exists. Querying without an index, or on a non-indexed field, raises an error.
41
+ - Indexes work only on top-level document fields, not nested paths.
42
+
43
+ Example:
44
+
45
+ ```json
46
+ {
47
+ "index_fields": ["email", "status"]
48
+ }
49
+ ```
50
+
51
+ For example, this works:
52
+
53
+ ```json
54
+ {
55
+ "email": "alice@example.com",
56
+ "profile": {
57
+ "city": "Rome"
58
+ }
59
+ }
60
+ ```
61
+
62
+ - you can index `email`
63
+ - you cannot index `profile.city`
64
+
65
+ ## CLI
66
+
67
+ Start the interactive shell:
68
+
69
+ ```bash
70
+ jsondocstore /path/to/store
71
+ ```
72
+
73
+ Commands:
74
+
75
+ - `list` prints document filenames only
76
+ - `listindexes` prints indexed fields, or `[]` if there is no `index.json`
77
+ - `queryby FIELD VALUE`
78
+ - `createindex FIELD`
79
+ - `deleteindex FIELD`
80
+ - `insert KEY JSON_DOCUMENT`
81
+ - `update KEY JSON_DOCUMENT`
82
+ - `delete KEY`
83
+ - `exit`
84
+
85
+ Example session:
86
+
87
+ ```text
88
+ $ jsondocstore ./data
89
+ jsondocstore> insert user-1 '{"username": "alice", "password": "secret1", "role": "admin"}'
90
+ jsondocstore> insert user-2 '{"username": "bob", "password": "secret2", "role": "user"}'
91
+ jsondocstore> insert user-3 '{"username": "carol", "password": "secret3", "role": "user"}'
92
+ jsondocstore> update user-2 '{"username": "bob", "password": "secret2", "role": "admin"}'
93
+ jsondocstore> list
94
+ [
95
+ "user-1.json",
96
+ "user-2.json",
97
+ "user-3.json"
98
+ ]
99
+ jsondocstore> createindex role
100
+ jsondocstore> queryby role user
101
+ [
102
+ {
103
+ "password": "secret2",
104
+ "role": "user",
105
+ "username": "bob"
106
+ },
107
+ {
108
+ "password": "secret3",
109
+ "role": "user",
110
+ "username": "carol"
111
+ }
112
+ ]
113
+ ```
114
+
115
+ ## Library
116
+
117
+ ```python
118
+ from jsondocstore import JsonDocStore
119
+
120
+ store = JsonDocStore("./data", create=True)
121
+ store.insert("user-1", {"username": "alice", "password": "secret1", "role": "admin"})
122
+ store.insert("user-2", {"username": "bob", "password": "secret2", "role": "user"})
123
+ store.insert("user-3", {"username": "carol", "password": "secret3", "role": "user"})
124
+ store.update("user-2", {"username": "bob", "password": "secret2", "role": "admin"})
125
+ names = store.list_all()
126
+ store.create_index("role")
127
+ admins = store.query_by("role", "admin")
128
+ doc = store.get("user-1")
129
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/jsondocstore/__init__.py
5
+ src/jsondocstore/cli.py
6
+ src/jsondocstore/core.py
7
+ src/jsondocstore.egg-info/PKG-INFO
8
+ src/jsondocstore.egg-info/SOURCES.txt
9
+ src/jsondocstore.egg-info/dependency_links.txt
10
+ src/jsondocstore.egg-info/entry_points.txt
11
+ src/jsondocstore.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jsondocstore = jsondocstore.cli:main
@@ -0,0 +1 @@
1
+ jsondocstore