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.
- blackkube-0.1.0/LICENSE +21 -0
- blackkube-0.1.0/PKG-INFO +13 -0
- blackkube-0.1.0/README.md +161 -0
- blackkube-0.1.0/blackkube.egg-info/PKG-INFO +13 -0
- blackkube-0.1.0/blackkube.egg-info/SOURCES.txt +32 -0
- blackkube-0.1.0/blackkube.egg-info/dependency_links.txt +1 -0
- blackkube-0.1.0/blackkube.egg-info/entry_points.txt +2 -0
- blackkube-0.1.0/blackkube.egg-info/requires.txt +5 -0
- blackkube-0.1.0/blackkube.egg-info/top_level.txt +2 -0
- blackkube-0.1.0/cli/__init__.py +0 -0
- blackkube-0.1.0/cli/__main__.py +4 -0
- blackkube-0.1.0/cli/app.py +182 -0
- blackkube-0.1.0/cli/commands.py +188 -0
- blackkube-0.1.0/cli/render.py +148 -0
- blackkube-0.1.0/cli/repl.py +145 -0
- blackkube-0.1.0/cli/server.py +310 -0
- blackkube-0.1.0/kube/__init__.py +67 -0
- blackkube-0.1.0/kube/_collection.py +192 -0
- blackkube-0.1.0/kube/_config.py +41 -0
- blackkube-0.1.0/kube/_crypto.py +47 -0
- blackkube-0.1.0/kube/_index.py +17 -0
- blackkube-0.1.0/kube/_query.py +75 -0
- blackkube-0.1.0/kube/_schema.py +88 -0
- blackkube-0.1.0/kube/_storage.py +56 -0
- blackkube-0.1.0/pyproject.toml +25 -0
- blackkube-0.1.0/setup.cfg +4 -0
- blackkube-0.1.0/tests/test_config.py +58 -0
- blackkube-0.1.0/tests/test_core.py +390 -0
- blackkube-0.1.0/tests/test_crypto.py +77 -0
- blackkube-0.1.0/tests/test_index.py +50 -0
- blackkube-0.1.0/tests/test_integration.py +131 -0
- blackkube-0.1.0/tests/test_query.py +180 -0
- blackkube-0.1.0/tests/test_schema.py +144 -0
- blackkube-0.1.0/tests/test_server.py +240 -0
blackkube-0.1.0/LICENSE
ADDED
|
@@ -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.
|
blackkube-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://www.python.org)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -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")
|