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 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
+ [![Latest PyPI package version](https://badge.fury.io/py/keldb.svg)](https://pypi.org/project/keldb) [![Latest Read The Docs](https://readthedocs.org/projects/keldb/badge/?version=latest)](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
+ [![Latest PyPI package version](https://badge.fury.io/py/keldb.svg)](https://pypi.org/project/keldb) [![Latest Read The Docs](https://readthedocs.org/projects/keldb/badge/?version=latest)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![Latest PyPI package version](https://badge.fury.io/py/keldb.svg)](https://pypi.org/project/keldb) [![Latest Read The Docs](https://readthedocs.org/projects/keldb/badge/?version=latest)](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,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/keldb/__init__.py
5
+ src/keldb.egg-info/PKG-INFO
6
+ src/keldb.egg-info/SOURCES.txt
7
+ src/keldb.egg-info/dependency_links.txt
8
+ src/keldb.egg-info/requires.txt
9
+ src/keldb.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ aiofiles>=22.1.0
@@ -0,0 +1 @@
1
+ keldb