keldb 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.
- keldb-0.1.0/LICENSE +24 -0
- keldb-0.1.0/PKG-INFO +52 -0
- keldb-0.1.0/README.md +41 -0
- keldb-0.1.0/pyproject.toml +13 -0
- keldb-0.1.0/setup.cfg +4 -0
- keldb-0.1.0/src/keldb/__init__.py +258 -0
- keldb-0.1.0/src/keldb.egg-info/PKG-INFO +52 -0
- keldb-0.1.0/src/keldb.egg-info/SOURCES.txt +9 -0
- keldb-0.1.0/src/keldb.egg-info/dependency_links.txt +1 -0
- keldb-0.1.0/src/keldb.egg-info/requires.txt +1 -0
- keldb-0.1.0/src/keldb.egg-info/top_level.txt +1 -0
keldb-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
keldb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keldb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: KelDB is a simple node-based database for asyncio applications.
|
|
5
|
+
Author-email: TriangularDev <triangle@dylsplazy.com>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: aiofiles>=22.1.0
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# KelDB
|
|
14
|
+
[](https://pypi.org/project/keldb) [](https://keldb.readthedocs.io/en/latest/)
|
|
15
|
+
|
|
16
|
+
KelDB is a simple node-based database for asyncio applications.
|
|
17
|
+
## Dynamics
|
|
18
|
+
KelDB is organised into **nodes**. A node is a container that can hold a value and/or other **subnodes**. The database itself is the root node, and every piece of data is a subnode of it.
|
|
19
|
+
## Usage
|
|
20
|
+
KelDB is quite flexible. There's only a few commands to learn.
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import keldb
|
|
24
|
+
|
|
25
|
+
# Create a default KelDB database (or load an existing database)
|
|
26
|
+
database = keldb.KelDB(keldb.FileStoreHook("./testdb/"))
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
# Create subnodes (lazy creation - no actual subnodes are created yet)
|
|
30
|
+
foo = await database.get_subnode("foo")
|
|
31
|
+
bar = await database.get_subnode("bar")
|
|
32
|
+
baz = await database.get_subnode("baz")
|
|
33
|
+
|
|
34
|
+
await bar.set_value("This can be any json-serializable object.") # Set a value (now "bar" is actually created)
|
|
35
|
+
|
|
36
|
+
await baz.set_value({"type": "user", "name": "Gabe Newell"}) # Now "baz" is actually created
|
|
37
|
+
|
|
38
|
+
text_subnode = await foo.get_subnode("text")
|
|
39
|
+
|
|
40
|
+
await text_subnode.set_value("If you are reading this, this data saved correctly!") # Write text in a subnode's subnode. (now both "foo" and "foo/text" are created)
|
|
41
|
+
|
|
42
|
+
print(await text_subnode.get_value()) # Read a value from the database
|
|
43
|
+
|
|
44
|
+
await foo.delete() # Delete a subnode (do note that this also recursively deletes any subnodes under it)
|
|
45
|
+
|
|
46
|
+
async for subnode in database.list_subnodes(): # Iterate over subnodes
|
|
47
|
+
print(subnode.path)
|
|
48
|
+
|
|
49
|
+
await database.set_value("Even the database itself is technically a node!")
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
|
keldb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
# KelDB
|
|
3
|
+
[](https://pypi.org/project/keldb) [](https://keldb.readthedocs.io/en/latest/)
|
|
4
|
+
|
|
5
|
+
KelDB is a simple node-based database for asyncio applications.
|
|
6
|
+
## Dynamics
|
|
7
|
+
KelDB is organised into **nodes**. A node is a container that can hold a value and/or other **subnodes**. The database itself is the root node, and every piece of data is a subnode of it.
|
|
8
|
+
## Usage
|
|
9
|
+
KelDB is quite flexible. There's only a few commands to learn.
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import keldb
|
|
13
|
+
|
|
14
|
+
# Create a default KelDB database (or load an existing database)
|
|
15
|
+
database = keldb.KelDB(keldb.FileStoreHook("./testdb/"))
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
# Create subnodes (lazy creation - no actual subnodes are created yet)
|
|
19
|
+
foo = await database.get_subnode("foo")
|
|
20
|
+
bar = await database.get_subnode("bar")
|
|
21
|
+
baz = await database.get_subnode("baz")
|
|
22
|
+
|
|
23
|
+
await bar.set_value("This can be any json-serializable object.") # Set a value (now "bar" is actually created)
|
|
24
|
+
|
|
25
|
+
await baz.set_value({"type": "user", "name": "Gabe Newell"}) # Now "baz" is actually created
|
|
26
|
+
|
|
27
|
+
text_subnode = await foo.get_subnode("text")
|
|
28
|
+
|
|
29
|
+
await text_subnode.set_value("If you are reading this, this data saved correctly!") # Write text in a subnode's subnode. (now both "foo" and "foo/text" are created)
|
|
30
|
+
|
|
31
|
+
print(await text_subnode.get_value()) # Read a value from the database
|
|
32
|
+
|
|
33
|
+
await foo.delete() # Delete a subnode (do note that this also recursively deletes any subnodes under it)
|
|
34
|
+
|
|
35
|
+
async for subnode in database.list_subnodes(): # Iterate over subnodes
|
|
36
|
+
print(subnode.path)
|
|
37
|
+
|
|
38
|
+
await database.set_value("Even the database itself is technically a node!")
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
41
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "keldb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "KelDB is a simple node-based database for asyncio applications."
|
|
9
|
+
authors = [{ name="TriangularDev", email="triangle@dylsplazy.com" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.7"
|
|
12
|
+
|
|
13
|
+
dependencies = ["aiofiles>=22.1.0"]
|
keldb-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KelDB
|
|
3
|
+
|
|
4
|
+
An asynchronous hierarchical key-value database abstraction.
|
|
5
|
+
|
|
6
|
+
KelDB organizes data as a tree of nodes. Each node can store a value and have subnodes. Storage is delegated to a backend Hook.
|
|
7
|
+
|
|
8
|
+
Core Components:
|
|
9
|
+
- Node: Represents a node in the database tree.
|
|
10
|
+
- Hook: Backend abstraction interface for custom setups.
|
|
11
|
+
- FileStoreHook: Filesystem-backed implementation.
|
|
12
|
+
- KelDB: Root database node.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, AsyncIterator, Optional
|
|
18
|
+
import aiofiles
|
|
19
|
+
import asyncio
|
|
20
|
+
import pathlib
|
|
21
|
+
import base64
|
|
22
|
+
import shutil
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =========================
|
|
28
|
+
# Node
|
|
29
|
+
# =========================
|
|
30
|
+
|
|
31
|
+
class Node:
|
|
32
|
+
"""
|
|
33
|
+
Represents a singular node.
|
|
34
|
+
|
|
35
|
+
This object is a lightweight reference to a database path. It can store a value and contain subnodes.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
root (KelDB): Root database instance.
|
|
39
|
+
parent (Node | None): Parent node.
|
|
40
|
+
name (str | None): Node name.
|
|
41
|
+
path (str | None): Full database path.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self.root: Optional["KelDB"] = None
|
|
46
|
+
self.parent: Optional["Node"] = None
|
|
47
|
+
self.name: Optional[str] = None
|
|
48
|
+
self.path: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
async def get_value(self) -> Any:
|
|
51
|
+
"""
|
|
52
|
+
Get the value of this node.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Any: Stored value or None.
|
|
56
|
+
"""
|
|
57
|
+
return await self.root.hook.get_path_value(
|
|
58
|
+
self.path,
|
|
59
|
+
cached=self.root.cache_enabled,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def set_value(self, value: Any) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Set the value of this node.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
value (Any): JSON-serializable value.
|
|
68
|
+
"""
|
|
69
|
+
await self.root.hook.set_path_value(
|
|
70
|
+
self.path,
|
|
71
|
+
value,
|
|
72
|
+
cached=self.root.cache_enabled,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def list_subnodes(self) -> AsyncIterator["Node"]:
|
|
76
|
+
"""
|
|
77
|
+
Iterate over subnodes.
|
|
78
|
+
|
|
79
|
+
Yields:
|
|
80
|
+
Node: Subnode objects.
|
|
81
|
+
"""
|
|
82
|
+
if not await self.exists():
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
async for subnode_name in self.root.hook.list_path_subpaths(
|
|
86
|
+
self.path,
|
|
87
|
+
cached=self.root.cache_enabled,
|
|
88
|
+
):
|
|
89
|
+
subnode = Node()
|
|
90
|
+
subnode.root = self.root
|
|
91
|
+
subnode.parent = self
|
|
92
|
+
subnode.name = subnode_name
|
|
93
|
+
subnode.path = self.path + f"{subnode_name}/"
|
|
94
|
+
yield subnode
|
|
95
|
+
|
|
96
|
+
async def get_subnode(self, subnode_name: str) -> "Node":
|
|
97
|
+
"""
|
|
98
|
+
Get a reference to a subnode.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
subnode_name (str): Name of subnode.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Node: Subnode reference.
|
|
105
|
+
"""
|
|
106
|
+
subnode = Node()
|
|
107
|
+
subnode.root = self.root
|
|
108
|
+
subnode.parent = self
|
|
109
|
+
subnode.name = subnode_name
|
|
110
|
+
subnode.path = self.path + f"{subnode_name}/"
|
|
111
|
+
return subnode
|
|
112
|
+
|
|
113
|
+
async def exists(self) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Check if this node exists.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: True if exists.
|
|
119
|
+
"""
|
|
120
|
+
return await self.root.hook.check_path_exists(
|
|
121
|
+
self.path,
|
|
122
|
+
cached=self.root.cache_enabled,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def delete(self) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Recursively delete this node and all subnodes.
|
|
128
|
+
"""
|
|
129
|
+
if await self.exists():
|
|
130
|
+
await self.root.hook.delete_path(
|
|
131
|
+
self.path,
|
|
132
|
+
cached=self.root.cache_enabled,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# =========================
|
|
137
|
+
# Hook Interface
|
|
138
|
+
# =========================
|
|
139
|
+
|
|
140
|
+
class Hook:
|
|
141
|
+
"""
|
|
142
|
+
Abstract storage backend interface.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
async def get_path_value(self, path: str, cached: bool = False) -> Any:
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
|
|
148
|
+
async def set_path_value(self, path: str, value: Any, cached: bool = False) -> Any:
|
|
149
|
+
raise NotImplementedError
|
|
150
|
+
|
|
151
|
+
async def list_path_subpaths(
|
|
152
|
+
self, path: str, cached: bool = False
|
|
153
|
+
) -> AsyncIterator[str]:
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
156
|
+
async def check_path_exists(self, path: str, cached: bool = False) -> bool:
|
|
157
|
+
raise NotImplementedError
|
|
158
|
+
|
|
159
|
+
async def delete_path(self, path: str, cached: bool = False) -> None:
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# =========================
|
|
164
|
+
# File Store Hook
|
|
165
|
+
# =========================
|
|
166
|
+
|
|
167
|
+
class FileStoreHook(Hook):
|
|
168
|
+
"""
|
|
169
|
+
Filesystem-backed storage implementation.
|
|
170
|
+
|
|
171
|
+
Each node is stored as a directory. The value is stored in value.json inside the directory.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, dir: str) -> None:
|
|
175
|
+
self.dir = pathlib.Path(dir).absolute()
|
|
176
|
+
self.locks = [asyncio.Lock() for _ in range(100)]
|
|
177
|
+
|
|
178
|
+
async def get_directory_lock(self, directory: str) -> asyncio.Lock:
|
|
179
|
+
return self.locks[hash(directory) % len(self.locks)]
|
|
180
|
+
|
|
181
|
+
async def get_path_directory(self, path: str, file: str = "") -> str:
|
|
182
|
+
parts = (
|
|
183
|
+
self.dir,
|
|
184
|
+
*[
|
|
185
|
+
base64.urlsafe_b64encode(x.encode()).decode()
|
|
186
|
+
for x in path.split("/")
|
|
187
|
+
if x
|
|
188
|
+
],
|
|
189
|
+
file,
|
|
190
|
+
)
|
|
191
|
+
return os.path.join(*parts)
|
|
192
|
+
|
|
193
|
+
async def get_path_value(self, path: str, cached: bool = False) -> Any:
|
|
194
|
+
file_path = await self.get_path_directory(path, "value.json")
|
|
195
|
+
|
|
196
|
+
async with await self.get_directory_lock(path):
|
|
197
|
+
if not os.path.isfile(file_path):
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
async with aiofiles.open(file_path, "r") as f:
|
|
201
|
+
return json.loads(await f.read())
|
|
202
|
+
|
|
203
|
+
async def set_path_value(self, path: str, value: Any, cached: bool = False) -> None:
|
|
204
|
+
directory = await self.get_path_directory(path)
|
|
205
|
+
|
|
206
|
+
async with await self.get_directory_lock(path):
|
|
207
|
+
os.makedirs(directory, exist_ok=True)
|
|
208
|
+
|
|
209
|
+
file_path = await self.get_path_directory(path, "value.json")
|
|
210
|
+
async with aiofiles.open(file_path, "w") as f:
|
|
211
|
+
await f.write(json.dumps(value))
|
|
212
|
+
|
|
213
|
+
async def list_path_subpaths(
|
|
214
|
+
self, path: str, cached: bool = False
|
|
215
|
+
) -> AsyncIterator[str]:
|
|
216
|
+
directory = await self.get_path_directory(path)
|
|
217
|
+
|
|
218
|
+
async with await self.get_directory_lock(path):
|
|
219
|
+
if not os.path.isdir(directory):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
with os.scandir(directory) as entries:
|
|
223
|
+
for entry in entries:
|
|
224
|
+
if entry.is_dir():
|
|
225
|
+
try:
|
|
226
|
+
yield base64.urlsafe_b64decode(entry.name.encode()).decode()
|
|
227
|
+
except Exception:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
async def check_path_exists(self, path: str, cached: bool = False) -> bool:
|
|
231
|
+
directory = await self.get_path_directory(path)
|
|
232
|
+
|
|
233
|
+
async with await self.get_directory_lock(path):
|
|
234
|
+
return os.path.isdir(directory)
|
|
235
|
+
|
|
236
|
+
async def delete_path(self, path: str, cached: bool = False) -> None:
|
|
237
|
+
directory = await self.get_path_directory(path)
|
|
238
|
+
|
|
239
|
+
async with await self.get_directory_lock(path):
|
|
240
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# =========================
|
|
244
|
+
# KelDB Root
|
|
245
|
+
# =========================
|
|
246
|
+
|
|
247
|
+
class KelDB(Node):
|
|
248
|
+
"""
|
|
249
|
+
Root database object.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, hook: Hook) -> None:
|
|
253
|
+
self.root = self
|
|
254
|
+
self.parent = None
|
|
255
|
+
self.name = ""
|
|
256
|
+
self.path = "/"
|
|
257
|
+
self.hook = hook
|
|
258
|
+
self.cache_enabled = True
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keldb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: KelDB is a simple node-based database for asyncio applications.
|
|
5
|
+
Author-email: TriangularDev <triangle@dylsplazy.com>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: aiofiles>=22.1.0
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# KelDB
|
|
14
|
+
[](https://pypi.org/project/keldb) [](https://keldb.readthedocs.io/en/latest/)
|
|
15
|
+
|
|
16
|
+
KelDB is a simple node-based database for asyncio applications.
|
|
17
|
+
## Dynamics
|
|
18
|
+
KelDB is organised into **nodes**. A node is a container that can hold a value and/or other **subnodes**. The database itself is the root node, and every piece of data is a subnode of it.
|
|
19
|
+
## Usage
|
|
20
|
+
KelDB is quite flexible. There's only a few commands to learn.
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import keldb
|
|
24
|
+
|
|
25
|
+
# Create a default KelDB database (or load an existing database)
|
|
26
|
+
database = keldb.KelDB(keldb.FileStoreHook("./testdb/"))
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
# Create subnodes (lazy creation - no actual subnodes are created yet)
|
|
30
|
+
foo = await database.get_subnode("foo")
|
|
31
|
+
bar = await database.get_subnode("bar")
|
|
32
|
+
baz = await database.get_subnode("baz")
|
|
33
|
+
|
|
34
|
+
await bar.set_value("This can be any json-serializable object.") # Set a value (now "bar" is actually created)
|
|
35
|
+
|
|
36
|
+
await baz.set_value({"type": "user", "name": "Gabe Newell"}) # Now "baz" is actually created
|
|
37
|
+
|
|
38
|
+
text_subnode = await foo.get_subnode("text")
|
|
39
|
+
|
|
40
|
+
await text_subnode.set_value("If you are reading this, this data saved correctly!") # Write text in a subnode's subnode. (now both "foo" and "foo/text" are created)
|
|
41
|
+
|
|
42
|
+
print(await text_subnode.get_value()) # Read a value from the database
|
|
43
|
+
|
|
44
|
+
await foo.delete() # Delete a subnode (do note that this also recursively deletes any subnodes under it)
|
|
45
|
+
|
|
46
|
+
async for subnode in database.list_subnodes(): # Iterate over subnodes
|
|
47
|
+
print(subnode.path)
|
|
48
|
+
|
|
49
|
+
await database.set_value("Even the database itself is technically a node!")
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiofiles>=22.1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keldb
|