blackkube 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) 2025 Marcos Gabriel Gomes
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,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: blackkube
3
+ Version: 0.1.0
4
+ Summary: JSONLine encrypted database with schema validation, CLI, and REST API
5
+ Project-URL: Homepage, https://github.com/marcosgabrielgomes110-collab/kube
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: cryptography>=41
9
+ Requires-Dist: fastapi>=0.100
10
+ Requires-Dist: uvicorn>=0.23
11
+ Requires-Dist: rich>=13
12
+ Requires-Dist: python-multipart>=0.0.6
13
+ Dynamic: license-file
@@ -0,0 +1,161 @@
1
+ # Blackkube
2
+
3
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org)
4
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+ [![Code style](https://img.shields.io/badge/code%20style-ruff-2ea44f)](https://github.com/astral-sh/ruff)
6
+
7
+ **Blackkube** is a JSON-line encrypted database for Python. Each collection is a flat file where records are individually encrypted with AES-256-GCM + PBKDF2 (600k rounds). It features schema validation via type hints, unique constraints, optional/default values, a rich CLI, and a REST API built with FastAPI.
8
+
9
+ ```python
10
+ from kube import Kube as kb, Model
11
+
12
+ class User(Model):
13
+ name = str
14
+ email = str
15
+ age = int
16
+
17
+ db = kb.storange("/tmp/db", password="minha-senha",
18
+ collections=[kb.define(User, unique=["email"])])
19
+
20
+ users = db.collections["user"]
21
+ uid = users.insert(name="Ana", email="ana@x.com", age=25)
22
+ print(users.get(uid))
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - **Encrypted at rest** — AES-256-GCM, unique salt per database, PBKDF2 600k rounds
28
+ - **Schema validation** — declare fields with Python type hints
29
+ - **Unique constraints** — enforced on insert and update
30
+ - **Optional & default values** — `kb.optional(str)` or `kb.default(str, "anonimo")`
31
+ - **Media fields** — `kb.media(max="5mb")` stores files with automatic MIME detection
32
+ - **Rich CLI** — one-shot commands and interactive REPL via `bk`
33
+ - **REST API** — FastAPI server with CORS, multipart upload, static file serving
34
+ - **Thread-safe** — all read/write operations protected by reentrant locks
35
+ - **Zero config** — databases are directories; open and go
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install blackkube
41
+ ```
42
+
43
+ Or from source:
44
+
45
+ ```bash
46
+ git clone https://github.com/anomalyco/blackkube.git
47
+ cd blackkube
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Quickstart
52
+
53
+ ### Python API
54
+
55
+ ```python
56
+ from kube import Kube as kb, Model
57
+
58
+ class Task(Model):
59
+ title = str
60
+ done = bool
61
+
62
+ with kb.storange("/tmp/tasks", password="secret",
63
+ collections=[kb.define(Task)]) as db:
64
+ todos = db.collections["task"]
65
+ tid = todos.insert(title="Buy milk", done=False)
66
+ todos.update(tid, done=True)
67
+ for t in todos.find():
68
+ print(t)
69
+ ```
70
+
71
+ ### CLI
72
+
73
+ ```bash
74
+ # One-shot commands
75
+ bk --path /tmp/db --password x user insert name=Ana age=25
76
+ bk --path /tmp/db --password x user find age__gte=18
77
+
78
+ # Interactive REPL
79
+ bk use /tmp/db
80
+ bk> user find
81
+ ```
82
+
83
+ ### REST API
84
+
85
+ ```bash
86
+ # Start server
87
+ bk --path /tmp/db server start
88
+
89
+ # Use the API
90
+ curl http://localhost:8888/user -d '{"name":"Ana","age":25}'
91
+ curl http://localhost:8888/user -X POST -F "avatar=@photo.jpg"
92
+ ```
93
+
94
+ ## Documentation
95
+
96
+ Full documentation is available in the [docs/](docs/) directory (Portuguese):
97
+
98
+ | Section | Description |
99
+ |---------|-------------|
100
+ | [CLI](docs/cli/) | `bk` command reference |
101
+ | [API](docs/api/) | Python API docs (`Kube`, `Collection`, `Query`, `Model`) |
102
+ | [Types](docs/types/) | Schema declaration (`field`, `optional`, `default`, `media`) |
103
+ | [Modules](docs/modules/) | Internal architecture |
104
+ | [Examples](docs/examples/) | Runnable example scripts |
105
+ | [Web API](docs/webapi/) | REST API reference |
106
+
107
+ ## Testing
108
+
109
+ ```bash
110
+ pip install -e ".[dev]"
111
+ pytest tests/
112
+ ```
113
+
114
+ ## How it works
115
+
116
+ Each database is a directory containing:
117
+
118
+ ```
119
+ /tmp/mydb/
120
+ ├── server.conf.json # Server config (host, port, status)
121
+ ├── .key # Salt + verification token
122
+ └── <collection>/
123
+ ├── schema.json # Field types, defaults, optional, unique, media
124
+ ├── data.jsonl # AES-256-GCM encrypted records (one per line)
125
+ ├── data.idx # Binary index (by_id + unique maps)
126
+ └── media/ # Uploaded media files (uuid.jpg)
127
+ ```
128
+
129
+ - Records are JSON, encrypted individually with AES-256-GCM, and stored one per line (`.jsonl`)
130
+ - The index file caches `by_id` offsets and unique value → `_id` mappings for O(1) lookups
131
+ - Media files are stored as `<uuid>.jpg`; the record contains only the filename
132
+ - Thread-safe via `threading.RLock()` on every collection operation
133
+
134
+ ## Project structure
135
+
136
+ ```
137
+ blackkube/
138
+ ├── kube/ # Core library
139
+ │ ├── _config.py # Constants and helpers
140
+ │ ├── _crypto.py # AES-256-GCM + PBKDF2
141
+ │ ├── _schema.py # Field types and validation
142
+ │ ├── _index.py # In-memory index
143
+ │ ├── _collection.py # Collection CRUD
144
+ │ ├── _query.py # Query with operators
145
+ │ ├── _storage.py # Persistence layer
146
+ │ └── __init__.py # Public API (Kube, Model, query)
147
+ ├── cli/ # CLI and REST server
148
+ │ ├── app.py # Argument parser and dispatch
149
+ │ ├── commands.py # Command handlers
150
+ │ ├── render.py # Rich output
151
+ │ ├── repl.py # Interactive REPL
152
+ │ ├── server.py # FastAPI application
153
+ │ └── __main__.py # Entry point
154
+ ├── tests/ # Test suite (139+ tests)
155
+ ├── docs/ # Documentation
156
+ └── pyproject.toml # Build configuration
157
+ ```
158
+
159
+ ## License
160
+
161
+ [MIT](LICENSE) © Marcos Gabriel Gomes
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: blackkube
3
+ Version: 0.1.0
4
+ Summary: JSONLine encrypted database with schema validation, CLI, and REST API
5
+ Project-URL: Homepage, https://github.com/marcosgabrielgomes110-collab/kube
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: cryptography>=41
9
+ Requires-Dist: fastapi>=0.100
10
+ Requires-Dist: uvicorn>=0.23
11
+ Requires-Dist: rich>=13
12
+ Requires-Dist: python-multipart>=0.0.6
13
+ Dynamic: license-file
@@ -0,0 +1,32 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ blackkube.egg-info/PKG-INFO
5
+ blackkube.egg-info/SOURCES.txt
6
+ blackkube.egg-info/dependency_links.txt
7
+ blackkube.egg-info/entry_points.txt
8
+ blackkube.egg-info/requires.txt
9
+ blackkube.egg-info/top_level.txt
10
+ cli/__init__.py
11
+ cli/__main__.py
12
+ cli/app.py
13
+ cli/commands.py
14
+ cli/render.py
15
+ cli/repl.py
16
+ cli/server.py
17
+ kube/__init__.py
18
+ kube/_collection.py
19
+ kube/_config.py
20
+ kube/_crypto.py
21
+ kube/_index.py
22
+ kube/_query.py
23
+ kube/_schema.py
24
+ kube/_storage.py
25
+ tests/test_config.py
26
+ tests/test_core.py
27
+ tests/test_crypto.py
28
+ tests/test_index.py
29
+ tests/test_integration.py
30
+ tests/test_query.py
31
+ tests/test_schema.py
32
+ tests/test_server.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bk = cli.__main__:main
@@ -0,0 +1,5 @@
1
+ cryptography>=41
2
+ fastapi>=0.100
3
+ uvicorn>=0.23
4
+ rich>=13
5
+ python-multipart>=0.0.6
@@ -0,0 +1,2 @@
1
+ cli
2
+ kube
File without changes
@@ -0,0 +1,4 @@
1
+ from .app import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,182 @@
1
+ import sys, os
2
+ from pathlib import Path
3
+ from getpass import getpass
4
+ from rich.traceback import install as rich_tb
5
+
6
+ from kube import Kube
7
+ from .render import console, error, info_msg, spinner, parse_keyvals
8
+ from .commands import (
9
+ cmd_collections, cmd_find, cmd_get, cmd_insert, cmd_update,
10
+ cmd_delete, cmd_count, cmd_schema, cmd_info,
11
+ cmd_export, cmd_import, cmd_vacuum, cmd_server,
12
+ )
13
+ from .repl import Repl
14
+
15
+ rich_tb(show_locals=False)
16
+
17
+
18
+ _COLLECTION_CMDS = {
19
+ "find": cmd_find,
20
+ "get": cmd_get,
21
+ "insert": cmd_insert,
22
+ "update": cmd_update,
23
+ "delete": cmd_delete,
24
+ "count": cmd_count,
25
+ "schema": cmd_schema,
26
+ "info": cmd_info,
27
+ "export": cmd_export,
28
+ "import": cmd_import,
29
+ }
30
+
31
+ _GLOBAL_CMDS = {
32
+ "collections": cmd_collections,
33
+ }
34
+
35
+
36
+ class App:
37
+ def __init__(self):
38
+ self._db = None
39
+
40
+ @property
41
+ def db(self):
42
+ if self._db is None:
43
+ error("nenhum database aberto. use: bk use <path>")
44
+ return self._db
45
+
46
+ def connect(self, path, password=None, quiet=False):
47
+ if password is None:
48
+ password = getpass("Password: ")
49
+ try:
50
+ with spinner("conectando..."):
51
+ self._db = Kube(Path(path), password)
52
+ if not quiet:
53
+ info_msg(f"conectado a {path}")
54
+ return self._db
55
+ except PermissionError:
56
+ error("senha incorreta")
57
+ return None
58
+ except Exception as e:
59
+ error(str(e))
60
+ return None
61
+
62
+ def close(self):
63
+ if self._db:
64
+ self._db.close()
65
+ self._db = None
66
+
67
+ def get_collection(self, name):
68
+ db = self.db
69
+ if db is None:
70
+ return None
71
+ if name not in db.collections:
72
+ cols = ", ".join(db.collections.keys())
73
+ error(f"coleção '{name}' não encontrada. collections: {cols}")
74
+ return None
75
+ return db.collections[name]
76
+
77
+
78
+ def _resolve_password(args_password):
79
+ if args_password:
80
+ return args_password
81
+ pw = os.environ.get("BK_PASSWORD")
82
+ if pw:
83
+ return pw
84
+ return None
85
+
86
+
87
+ def main():
88
+ args = sys.argv[1:]
89
+ if not args:
90
+ Repl(App()).run()
91
+ return
92
+
93
+ path = None
94
+ password = None
95
+ rest = []
96
+
97
+ i = 0
98
+ while i < len(args):
99
+ a = args[i]
100
+ if a in ("--path", "-p"):
101
+ i += 1
102
+ if i < len(args):
103
+ path = args[i]
104
+ elif a in ("--password", "-P"):
105
+ i += 1
106
+ if i < len(args):
107
+ password = args[i]
108
+ else:
109
+ rest.append(a)
110
+ i += 1
111
+
112
+ app = App()
113
+ cmd = rest[0] if rest else None
114
+ cmd_args = rest[1:] if rest else []
115
+
116
+ if cmd == "server":
117
+ sub = cmd_args[0] if cmd_args else "status"
118
+ sub_args = cmd_args[1:] if len(cmd_args) > 1 else []
119
+ sub_filters = parse_keyvals(sub_args) or {}
120
+ if path:
121
+ sub_filters["_path"] = path
122
+ if _resolve_password(password):
123
+ sub_filters["_password"] = _resolve_password(password)
124
+ cmd_server(app, sub, sub_filters)
125
+ return
126
+
127
+ if path:
128
+ pw = _resolve_password(password)
129
+ quiet = bool(rest)
130
+ app.connect(path, pw, quiet=quiet)
131
+ if app._db is None:
132
+ return
133
+
134
+ if not rest:
135
+ Repl(app).run()
136
+ return
137
+
138
+ if cmd in _GLOBAL_CMDS:
139
+ if cmd == "collections":
140
+ if app._db is None:
141
+ error("nenhum database aberto. use: bk use <path> primeiro")
142
+ return
143
+ _GLOBAL_CMDS[cmd](app)
144
+ app.close()
145
+ return
146
+
147
+ if cmd == "use":
148
+ if not cmd_args:
149
+ error("uso: bk use <path>")
150
+ return
151
+ app.connect(cmd_args[0])
152
+ return
153
+
154
+ if cmd == "vacuum":
155
+ if app._db is None:
156
+ error("nenhum database aberto. use: bk use <path> primeiro")
157
+ return
158
+ cmd_vacuum(app, cmd_args[0] if cmd_args else None)
159
+ app.close()
160
+ return
161
+
162
+ name = cmd
163
+ sub = cmd_args[0] if cmd_args else None
164
+ sub_args = cmd_args[1:] if cmd_args else []
165
+
166
+ if not sub or sub not in _COLLECTION_CMDS and sub not in _GLOBAL_CMDS:
167
+ valid = ", ".join(_COLLECTION_CMDS)
168
+ error(f"comando inválido para coleção '{name}'. comandos: {valid}")
169
+ return
170
+
171
+ if sub == "collections":
172
+ _GLOBAL_CMDS[sub](app)
173
+ app.close()
174
+ return
175
+
176
+ filters = parse_keyvals(sub_args)
177
+ if filters is None:
178
+ return
179
+
180
+ handler = _COLLECTION_CMDS[sub]
181
+ handler(app, name, filters)
182
+ app.close()
@@ -0,0 +1,188 @@
1
+ import sys, os, json
2
+ from kube import query as qry
3
+ from .render import table, record, collections, schema, info as info_view, ok, error, count, spinner, confirm_action
4
+ from . import server as srv
5
+
6
+
7
+ def cmd_collections(app):
8
+ with spinner("carregando coleções..."):
9
+ colls = app.db.collections
10
+ collections(colls)
11
+
12
+
13
+ def cmd_find(app, name, filters):
14
+ col = app.get_collection(name)
15
+ if not col:
16
+ return
17
+ try:
18
+ q = qry(col)
19
+ rs = q.find(**filters) if filters else col.find()
20
+ except ValueError as e:
21
+ error(str(e))
22
+ return
23
+ table(rs, title=name)
24
+
25
+
26
+ def cmd_get(app, name, filters):
27
+ col = app.get_collection(name)
28
+ if not col:
29
+ return
30
+ if not filters:
31
+ error("uso: bk <collection> get campo=valor [...]")
32
+ return
33
+ try:
34
+ r = qry(col).get(**filters)
35
+ except ValueError as e:
36
+ error(str(e))
37
+ return
38
+ record(r, title=name)
39
+
40
+
41
+ def cmd_insert(app, name, data):
42
+ col = app.get_collection(name)
43
+ if not col:
44
+ return
45
+ if not data:
46
+ error("uso: bk <collection> insert campo=valor [...]")
47
+ return
48
+ try:
49
+ with spinner("inserindo..."):
50
+ _id = col.insert(**data)
51
+ except (ValueError, TypeError) as e:
52
+ error(str(e))
53
+ return
54
+ ok(f"inserido [{col._name}] _id={_id}")
55
+
56
+
57
+ def cmd_update(app, name, filters):
58
+ col = app.get_collection(name)
59
+ if not col:
60
+ return
61
+ if "_id" not in filters:
62
+ error("uso: bk <collection> update _id=X campo=valor [...]")
63
+ return
64
+ _id = filters.pop("_id")
65
+ if not filters:
66
+ error("forneça ao menos um campo para alterar")
67
+ return
68
+ try:
69
+ r = col.update(_id, **filters)
70
+ except ValueError as e:
71
+ error(str(e))
72
+ return
73
+ if r is None:
74
+ error("registro não encontrado")
75
+ else:
76
+ ok("atualizado")
77
+
78
+
79
+ def cmd_delete(app, name, filters):
80
+ col = app.get_collection(name)
81
+ if not col:
82
+ return
83
+ _id = filters.get("_id")
84
+ if not _id:
85
+ error("uso: bk <collection> delete _id=X")
86
+ return
87
+ if not confirm_action(f"deletar _id={_id} em '{name}'?"):
88
+ return
89
+ ok("deletado" if col.delete(_id) else error("registro não encontrado"))
90
+
91
+
92
+ def cmd_count(app, name, filters):
93
+ col = app.get_collection(name)
94
+ if not col:
95
+ return
96
+ try:
97
+ n = qry(col).count(**filters) if filters else len(col._idx.by_id)
98
+ except ValueError as e:
99
+ error(str(e))
100
+ return
101
+ count(n)
102
+
103
+
104
+ def cmd_schema(app, name, _=None):
105
+ col = app.get_collection(name)
106
+ if not col:
107
+ return
108
+ schema(col._types, col._defaults, col._optional, col._unique)
109
+
110
+
111
+ def cmd_info(app, name, _=None):
112
+ col = app.get_collection(name)
113
+ if not col:
114
+ return
115
+ info_view(col)
116
+
117
+
118
+ def cmd_export(app, name, _):
119
+ col = app.get_collection(name)
120
+ if not col:
121
+ return
122
+ rs = col.find()
123
+ json.dump(rs, sys.stdout, indent=2, ensure_ascii=False)
124
+ sys.stdout.write("\n")
125
+
126
+
127
+ def cmd_import(app, name, _):
128
+ col = app.get_collection(name)
129
+ if not col:
130
+ return
131
+ data = json.load(sys.stdin)
132
+ if isinstance(data, dict):
133
+ data = [data]
134
+ n = 0
135
+ with spinner("importando..."):
136
+ for item in data:
137
+ _id = item.pop("_id", None)
138
+ item.pop("created_at", None)
139
+ item.pop("updated_at", None)
140
+ try:
141
+ col.insert(**item)
142
+ n += 1
143
+ except (ValueError, TypeError) as e:
144
+ warn(f"falha ao importar: {e}")
145
+ ok(f"{n} registro(s) importados")
146
+
147
+
148
+ def cmd_vacuum(app, name=None):
149
+ colls = [(n, c) for n, c in app.db.collections.items() if not name or n == name]
150
+ if name and not colls:
151
+ error(f"coleção '{name}' não encontrada")
152
+ return
153
+ for n, c in colls:
154
+ with spinner(f"vacuando {n}..."):
155
+ c.vacuum()
156
+ ok(f"{n} otimizado")
157
+
158
+
159
+ def cmd_server(app, sub, args):
160
+ db_path = str(app._storage._path) if app._db else args.get("_path", "")
161
+ if not db_path:
162
+ error("uso: bk --path /db server <comando>")
163
+ return
164
+
165
+ if sub == "init":
166
+ srv.init_conf(db_path)
167
+ elif sub == "start":
168
+ conf = srv.load_conf(db_path)
169
+ password = args.get("_password") or os.environ.get("BK_SERVER_PASSWORD") or os.environ.get("BK_PASSWORD")
170
+ if not password:
171
+ error("forneca a senha via --password ou BK_SERVER_PASSWORD env var")
172
+ return
173
+ host = args.get("host", conf["host"])
174
+ port = int(args.get("port", conf["port"]))
175
+ srv.start(db_path, password, host, port)
176
+ elif sub == "stop":
177
+ srv.stop(db_path)
178
+ elif sub == "status" or not sub:
179
+ srv.status_cmd(db_path)
180
+ elif sub == "set":
181
+ k = args.get("key") or next((k for k in ("host", "port", "static_dir") if k in args), None)
182
+ v = args.get("value") or args.get(k)
183
+ if not k or not v:
184
+ error("uso: bk server set host=X|port=Y|static_dir=Z")
185
+ return
186
+ srv.set_conf(db_path, k, v)
187
+ else:
188
+ error(f"subcomando desconhecido: '{sub}'. use: init|start|stop|status|set")