pybasedb-json 0.2.0__py3-none-any.whl
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.
- pybase/__init__.py +13 -0
- pybase/collections.py +133 -0
- pybase/database.py +72 -0
- pybase/exceptions.py +14 -0
- pybase/query.py +73 -0
- pybase/utils.py +11 -0
- pybasedb_json-0.2.0.dist-info/METADATA +92 -0
- pybasedb_json-0.2.0.dist-info/RECORD +10 -0
- pybasedb_json-0.2.0.dist-info/WHEEL +5 -0
- pybasedb_json-0.2.0.dist-info/top_level.txt +1 -0
pybase/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .collections import Collection
|
|
2
|
+
from .database import Database
|
|
3
|
+
from .exceptions import CollectionError, DatabaseError, PybaseError
|
|
4
|
+
from .query import Query
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Database",
|
|
8
|
+
"Collection",
|
|
9
|
+
"Query",
|
|
10
|
+
"PybaseError",
|
|
11
|
+
"DatabaseError",
|
|
12
|
+
"CollectionError",
|
|
13
|
+
]
|
pybase/collections.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .exceptions import CollectionError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Collection:
|
|
9
|
+
def __init__(self, file_path: str | Path) -> None:
|
|
10
|
+
self.file_path = Path(file_path)
|
|
11
|
+
self._lock = threading.Lock()
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return str(self.file_path)
|
|
15
|
+
|
|
16
|
+
def __len__(self) -> int:
|
|
17
|
+
return len(self._load())
|
|
18
|
+
|
|
19
|
+
def __iter__(self):
|
|
20
|
+
return iter(self._load())
|
|
21
|
+
|
|
22
|
+
def __getitem__(self, index: int | slice) -> dict | list[dict]:
|
|
23
|
+
return self._load()[index]
|
|
24
|
+
|
|
25
|
+
# -- io interno (sem lock) --
|
|
26
|
+
|
|
27
|
+
def _load_nolock(self) -> list:
|
|
28
|
+
if not self.file_path.exists():
|
|
29
|
+
return []
|
|
30
|
+
try:
|
|
31
|
+
with open(self.file_path, "r", encoding="utf-8") as f:
|
|
32
|
+
data = json.load(f)
|
|
33
|
+
except json.JSONDecodeError:
|
|
34
|
+
raise CollectionError(
|
|
35
|
+
f"arquivo corrompido: {self.file_path}"
|
|
36
|
+
)
|
|
37
|
+
if not isinstance(data, list):
|
|
38
|
+
raise CollectionError(
|
|
39
|
+
f"esperava uma lista, recebeu {type(data).__name__}"
|
|
40
|
+
f": {self.file_path}"
|
|
41
|
+
)
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
def _save_nolock(self, data: list) -> None:
|
|
45
|
+
with open(self.file_path, "w", encoding="utf-8") as f:
|
|
46
|
+
json.dump(data, f, indent=4)
|
|
47
|
+
|
|
48
|
+
def _load(self) -> list:
|
|
49
|
+
with self._lock:
|
|
50
|
+
return self._load_nolock()
|
|
51
|
+
|
|
52
|
+
def _save(self, data: list) -> None:
|
|
53
|
+
with self._lock:
|
|
54
|
+
self._save_nolock(data)
|
|
55
|
+
|
|
56
|
+
# -- leitura (thread-safe via _load) --
|
|
57
|
+
|
|
58
|
+
def all(self) -> list[dict]:
|
|
59
|
+
return list(self._load())
|
|
60
|
+
|
|
61
|
+
def count(self, **match) -> int:
|
|
62
|
+
if not match:
|
|
63
|
+
return len(self._load())
|
|
64
|
+
docs = self._load()
|
|
65
|
+
return sum(
|
|
66
|
+
1
|
|
67
|
+
for doc in docs
|
|
68
|
+
if all(doc.get(k) == v for k, v in match.items())
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get(self, doc_id: str | int, key: str = "id") -> dict | None:
|
|
72
|
+
for doc in self._load():
|
|
73
|
+
if doc.get(key) == doc_id:
|
|
74
|
+
return doc
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# -- escrita (atomica via lock unico) --
|
|
78
|
+
|
|
79
|
+
def add(self, data: dict) -> dict:
|
|
80
|
+
doc = dict(data)
|
|
81
|
+
with self._lock:
|
|
82
|
+
docs = self._load_nolock()
|
|
83
|
+
docs.append(doc)
|
|
84
|
+
self._save_nolock(docs)
|
|
85
|
+
return doc
|
|
86
|
+
|
|
87
|
+
def add_many(self, items: list[dict]) -> list[dict]:
|
|
88
|
+
if not isinstance(items, list):
|
|
89
|
+
raise TypeError("add_many espera uma lista de dicionarios")
|
|
90
|
+
with self._lock:
|
|
91
|
+
docs = self._load_nolock()
|
|
92
|
+
added = [dict(item) for item in items]
|
|
93
|
+
docs.extend(added)
|
|
94
|
+
self._save_nolock(docs)
|
|
95
|
+
return added
|
|
96
|
+
|
|
97
|
+
def actualize(self, data: dict, **match) -> int:
|
|
98
|
+
if not match:
|
|
99
|
+
raise CollectionError(
|
|
100
|
+
"informe um campo de busca, "
|
|
101
|
+
"ex: actualize({'age': 20}, name='marcos')"
|
|
102
|
+
)
|
|
103
|
+
with self._lock:
|
|
104
|
+
docs = self._load_nolock()
|
|
105
|
+
updated = 0
|
|
106
|
+
for doc in docs:
|
|
107
|
+
if all(doc.get(k) == v for k, v in match.items()):
|
|
108
|
+
doc.update(data)
|
|
109
|
+
updated += 1
|
|
110
|
+
if updated:
|
|
111
|
+
self._save_nolock(docs)
|
|
112
|
+
return updated
|
|
113
|
+
|
|
114
|
+
def rem(self, **match) -> int:
|
|
115
|
+
if not match:
|
|
116
|
+
raise CollectionError(
|
|
117
|
+
"informe um campo de busca, ex: rem(name='marcos')"
|
|
118
|
+
)
|
|
119
|
+
with self._lock:
|
|
120
|
+
docs = self._load_nolock()
|
|
121
|
+
kept = [
|
|
122
|
+
doc
|
|
123
|
+
for doc in docs
|
|
124
|
+
if not all(doc.get(k) == v for k, v in match.items())
|
|
125
|
+
]
|
|
126
|
+
removed = len(docs) - len(kept)
|
|
127
|
+
if removed:
|
|
128
|
+
self._save_nolock(kept)
|
|
129
|
+
return removed
|
|
130
|
+
|
|
131
|
+
def drop(self) -> None:
|
|
132
|
+
with self._lock:
|
|
133
|
+
self._save_nolock([])
|
pybase/database.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .collections import Collection
|
|
6
|
+
from .exceptions import DatabaseError
|
|
7
|
+
from .utils import Utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Database:
|
|
11
|
+
def __init__(self, local: str | Path, name: str, password: str) -> None:
|
|
12
|
+
self.local = Path(local)
|
|
13
|
+
self.name = name
|
|
14
|
+
self.password = str(Utils.encode(password))
|
|
15
|
+
|
|
16
|
+
self.path_dir = self.local / self.name
|
|
17
|
+
self.path_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
self.path_conf = self.path_dir / f"{name}_conf.json"
|
|
19
|
+
if not self.path_conf.exists():
|
|
20
|
+
self.path_conf.write_text(
|
|
21
|
+
json.dumps({"password": self.password}, indent=4)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return str(self.path_dir)
|
|
26
|
+
|
|
27
|
+
def _verify_password(self, password: str) -> dict:
|
|
28
|
+
try:
|
|
29
|
+
with open(self.path_conf, "r", encoding="utf-8") as f:
|
|
30
|
+
dados = json.load(f)
|
|
31
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
32
|
+
raise DatabaseError(
|
|
33
|
+
f"database '{self.name}' corrompido ou inexistente"
|
|
34
|
+
)
|
|
35
|
+
if str(Utils.encode(password)) != dados.get("password"):
|
|
36
|
+
raise DatabaseError("senha incorreta")
|
|
37
|
+
return dados
|
|
38
|
+
|
|
39
|
+
def open(self, password: str) -> "Database":
|
|
40
|
+
self._verify_password(password)
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def _table_paths(self) -> list[Path]:
|
|
44
|
+
return list(self.path_dir.glob("*_table.json"))
|
|
45
|
+
|
|
46
|
+
def info(self) -> dict:
|
|
47
|
+
tables = self._table_paths()
|
|
48
|
+
return {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"path": str(self.path_dir),
|
|
51
|
+
"collections": [
|
|
52
|
+
p.stem.removesuffix("_table") for p in tables
|
|
53
|
+
],
|
|
54
|
+
"size": sum(
|
|
55
|
+
p.stat().st_size for p in tables if p.is_file()
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def collections(self) -> list[str]:
|
|
60
|
+
return [p.stem.removesuffix("_table") for p in self._table_paths()]
|
|
61
|
+
|
|
62
|
+
def collection(self, name: str) -> Collection:
|
|
63
|
+
path = self.path_dir / f"{name}_table.json"
|
|
64
|
+
if not path.exists():
|
|
65
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump([], f, indent=4)
|
|
67
|
+
return Collection(path)
|
|
68
|
+
|
|
69
|
+
def drop(self, password: str) -> None:
|
|
70
|
+
self._verify_password(password)
|
|
71
|
+
if self.path_dir.exists():
|
|
72
|
+
shutil.rmtree(self.path_dir)
|
pybase/exceptions.py
ADDED
pybase/query.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from .collections import Collection
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Query:
|
|
5
|
+
def __init__(self, collection: Collection) -> None:
|
|
6
|
+
self._collection = collection
|
|
7
|
+
self._limit: int | None = None
|
|
8
|
+
self._offset: int | None = None
|
|
9
|
+
self._sort_key: str | None = None
|
|
10
|
+
self._sort_reverse: bool = False
|
|
11
|
+
|
|
12
|
+
def limit(self, n: int) -> "Query":
|
|
13
|
+
self._limit = n
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def offset(self, n: int) -> "Query":
|
|
17
|
+
self._offset = n
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def sort(self, key: str, reverse: bool = False) -> "Query":
|
|
21
|
+
self._sort_key = key
|
|
22
|
+
self._sort_reverse = reverse
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def _apply(self, docs: list[dict]) -> list[dict]:
|
|
26
|
+
if self._sort_key:
|
|
27
|
+
docs = sorted(
|
|
28
|
+
docs,
|
|
29
|
+
key=lambda d: d.get(self._sort_key, 0),
|
|
30
|
+
reverse=self._sort_reverse,
|
|
31
|
+
)
|
|
32
|
+
if self._offset is not None:
|
|
33
|
+
docs = docs[self._offset :]
|
|
34
|
+
if self._limit is not None:
|
|
35
|
+
docs = docs[: self._limit]
|
|
36
|
+
return docs
|
|
37
|
+
|
|
38
|
+
def _build(self, **match) -> list[dict]:
|
|
39
|
+
docs = self._collection.all()
|
|
40
|
+
if match:
|
|
41
|
+
docs = [
|
|
42
|
+
doc
|
|
43
|
+
for doc in docs
|
|
44
|
+
if all(doc.get(k) == v for k, v in match.items())
|
|
45
|
+
]
|
|
46
|
+
return self._apply(docs)
|
|
47
|
+
|
|
48
|
+
def _reset(self) -> None:
|
|
49
|
+
self._limit = None
|
|
50
|
+
self._offset = None
|
|
51
|
+
self._sort_key = None
|
|
52
|
+
self._sort_reverse = False
|
|
53
|
+
|
|
54
|
+
def find(self, **match) -> list[dict]:
|
|
55
|
+
result = self._build(**match)
|
|
56
|
+
self._reset()
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
def find_one(self, **match) -> dict | None:
|
|
60
|
+
docs = self._collection.all()
|
|
61
|
+
result = None
|
|
62
|
+
if not match:
|
|
63
|
+
result = docs[0] if docs else None
|
|
64
|
+
else:
|
|
65
|
+
for doc in docs:
|
|
66
|
+
if all(doc.get(k) == v for k, v in match.items()):
|
|
67
|
+
result = doc
|
|
68
|
+
break
|
|
69
|
+
self._reset()
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def exists(self, **match) -> bool:
|
|
73
|
+
return self.find_one(**match) is not None
|
pybase/utils.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pybasedb-json
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Banco de dados local com JSON — zero dependencias, 100% stdlib, thread-safe
|
|
5
|
+
Author-email: Marcos Gomes <marcosgabrielgomes110@gmail.com>
|
|
6
|
+
Maintainer-email: Marcos Gomes <marcosgabrielgomes110@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/marcosgabrielgomes110-collab/Pybase
|
|
9
|
+
Project-URL: Documentation, https://github.com/marcosgabrielgomes110-collab/Pybase/tree/main/docs
|
|
10
|
+
Project-URL: Repository, https://github.com/marcosgabrielgomes110-collab/Pybase
|
|
11
|
+
Keywords: database,json,local,embedded,nosql,thread-safe
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="site/assets/logo/logo.png" width="120" alt="Pybase logo">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
# Pybase
|
|
29
|
+
|
|
30
|
+
Base para criacao de bancos de dados locais com JSON. Zero dependencias, 100% stdlib.
|
|
31
|
+
Operacoes de escrita atomicas, thread-safe.
|
|
32
|
+
|
|
33
|
+
## Instalacao
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install pybasedb-json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Uso
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pybase import Database, Query
|
|
43
|
+
|
|
44
|
+
db = Database("./data", "mydb", "minha-senha").open("minha-senha")
|
|
45
|
+
|
|
46
|
+
users = db.collection("users")
|
|
47
|
+
|
|
48
|
+
users.add({"id": "a1", "name": "marcos", "age": 25})
|
|
49
|
+
users.add_many([
|
|
50
|
+
{"id": "a2", "name": "ana", "age": 30},
|
|
51
|
+
{"id": "a3", "name": "marcos", "age": 40},
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
print(len(users)) # 3
|
|
55
|
+
print(users.count(name="marcos")) # 2
|
|
56
|
+
|
|
57
|
+
for doc in users:
|
|
58
|
+
print(doc["name"]) # marcos, ana, marcos
|
|
59
|
+
|
|
60
|
+
doc = users.get("a1") # por id
|
|
61
|
+
print(doc) # {"id": "a1", "name": "marcos", "age": 25}
|
|
62
|
+
|
|
63
|
+
q = Query(users)
|
|
64
|
+
print(q.find(name="marcos")) # [doc_a1, doc_a3]
|
|
65
|
+
print(q.sort("age", reverse=True).limit(1).find()) # [doc_a3]
|
|
66
|
+
print(q.find_one(name="ana")) # {"id": "a2", ...}
|
|
67
|
+
print(q.exists(name="joao")) # False
|
|
68
|
+
|
|
69
|
+
users.actualize({"age": 26}, name="marcos")
|
|
70
|
+
users.rem(id="a3")
|
|
71
|
+
|
|
72
|
+
users.drop() # limpa colecao
|
|
73
|
+
# db.drop("minha-senha") # deleta banco inteiro
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Estrutura
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Pybase/
|
|
80
|
+
pybase/
|
|
81
|
+
__init__.py # exports: Database, Collection, Query, PybaseError, ...
|
|
82
|
+
database.py # Database: gerencia diretorio de tabelas JSON
|
|
83
|
+
collections.py # Collection: CRUD atomico com lock por colecao
|
|
84
|
+
query.py # Query: find/find_one/exists + sort/limit/offset
|
|
85
|
+
exceptions.py # PybaseError > DatabaseError, CollectionError
|
|
86
|
+
utils.py # Utils: sha256 encode
|
|
87
|
+
pyproject.toml
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Documentacao
|
|
91
|
+
|
|
92
|
+
Veja [docs/](docs/index.md) com referencia completa de cada metodo.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pybase/__init__.py,sha256=JDIhb8bxyo8Y77xPOgDKtYey5-TxLwhPY1pEPxXrxRo,285
|
|
2
|
+
pybase/collections.py,sha256=yRsPuyLzyqKHSDi7aReuWEpBsNUzHKghoYoAzUo9dNc,3880
|
|
3
|
+
pybase/database.py,sha256=Ew8-y_gRXl8eINY4VG-taQ8AO5SGEx5vwPKry6D4odA,2355
|
|
4
|
+
pybase/exceptions.py,sha256=_asagITkRr0O6GY5uFZmanNqSulBBbjp_8JOAGG-rgo,188
|
|
5
|
+
pybase/query.py,sha256=PCro0RWcdWpnu-_kX67gL7E0FuMK1oU6aHmQmeD3goA,2093
|
|
6
|
+
pybase/utils.py,sha256=RNmaFYwECMlIp1ifAILAj_gtEeb7kLNvP1qs_RMBJZs,159
|
|
7
|
+
pybasedb_json-0.2.0.dist-info/METADATA,sha256=KNpcWxzPEd26ut3GAAsUZzUt_i3VVGoazPi0zYuRMWM,2981
|
|
8
|
+
pybasedb_json-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
pybasedb_json-0.2.0.dist-info/top_level.txt,sha256=gYgKDnq77vSbZG-_hkku2uwMNI_t6Z7_WQq4dek8Al4,7
|
|
10
|
+
pybasedb_json-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pybase
|