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 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
@@ -0,0 +1,14 @@
1
+ class PybaseError(Exception):
2
+ pass
3
+
4
+
5
+ class DatabaseError(PybaseError):
6
+ pass
7
+
8
+
9
+ class CollectionError(PybaseError):
10
+ pass
11
+
12
+
13
+ class DocumentNotFoundError(CollectionError):
14
+ pass
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,11 @@
1
+ from hashlib import sha256
2
+
3
+
4
+ class Utils:
5
+
6
+ @staticmethod
7
+ def encode(text: str) -> str:
8
+ return sha256(text.encode()).hexdigest()
9
+
10
+
11
+ Utly = Utils
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pybase