pybasedb-json 0.3.0__tar.gz → 0.4.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.
Files changed (29) hide show
  1. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/PKG-INFO +59 -47
  2. pybasedb_json-0.4.0/README.md +120 -0
  3. pybasedb_json-0.4.0/pybase/__init__.py +44 -0
  4. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/backup.py +3 -2
  5. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/collections.py +118 -27
  6. pybasedb_json-0.4.0/pybase/crypto.py +59 -0
  7. pybasedb_json-0.4.0/pybase/database.py +151 -0
  8. pybasedb_json-0.4.0/pybase/exceptions.py +30 -0
  9. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/hooks.py +50 -35
  10. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/media.py +2 -2
  11. pybasedb_json-0.4.0/pybase/pybase.py +57 -0
  12. pybasedb_json-0.4.0/pybase/query.py +28 -0
  13. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/server.py +59 -116
  14. pybasedb_json-0.4.0/pybase/utils.py +23 -0
  15. pybasedb_json-0.4.0/pybase/validation.py +382 -0
  16. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybasedb_json.egg-info/PKG-INFO +59 -47
  17. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybasedb_json.egg-info/SOURCES.txt +2 -0
  18. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pyproject.toml +2 -2
  19. pybasedb_json-0.3.0/README.md +0 -108
  20. pybasedb_json-0.3.0/pybase/__init__.py +0 -29
  21. pybasedb_json-0.3.0/pybase/database.py +0 -106
  22. pybasedb_json-0.3.0/pybase/exceptions.py +0 -14
  23. pybasedb_json-0.3.0/pybase/query.py +0 -89
  24. pybasedb_json-0.3.0/pybase/utils.py +0 -8
  25. pybasedb_json-0.3.0/pybase/validation.py +0 -236
  26. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybase/index.py +0 -0
  27. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybasedb_json.egg-info/dependency_links.txt +0 -0
  28. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/pybasedb_json.egg-info/top_level.txt +0 -0
  29. {pybasedb_json-0.3.0 → pybasedb_json-0.4.0}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybasedb-json
3
- Version: 0.3.0
4
- Summary: Banco de dados local com JSON — schemas, server REST, media, zero dependencias
3
+ Version: 0.4.0
4
+ Summary: Banco de dados local com JSON — schemas, decorator API, server REST, criptografia, zero dependencias
5
5
  Author-email: Marcos Gomes <marcosgabrielgomes110@gmail.com>
6
6
  Maintainer-email: Marcos Gomes <marcosgabrielgomes110@gmail.com>
7
7
  License: MIT
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
28
28
  <img src="assets/logo/logo.png" width="120" alt="Pybase logo">
29
29
  </p>
30
30
 
31
- # Pybase
31
+ # Pybase v0.4
32
32
 
33
33
  Base para criacao de bancos de dados locais com JSON. Zero dependencias, 100% stdlib.
34
34
  Operacoes de escrita atomicas, thread-safe.
@@ -49,32 +49,39 @@ pip install Flask
49
49
  ## Uso rapido
50
50
 
51
51
  ```python
52
- from pybase import Database, Query, Server
53
- from pybase.validation import Schema
52
+ from pybase import pybase as pb
54
53
 
55
- # Banco de dados
56
- db = Database("./data", "mydb", "senha").open("senha")
54
+ # Banco de dados (sem senha ou com senha)
55
+ db = pb.Database("./data/mydb")
56
+ # db = pb.Database("./data/mydb", password="senha").open("senha")
57
57
 
58
- # Colecao com schema (opcional)
59
- db.schema("users", Schema({
60
- "id": str, "name": str, "email": str,
61
- "age": (int, 0), "active": (bool, True),
62
- }))
63
- users = db.collection("users")
64
- users.add({"id": "1", "name": "marcos", "email": "m@m.com"})
58
+ # Schema com decorator (v0.4+)
59
+ @db.schema.users
60
+ class User:
61
+ name: str
62
+ email: str
63
+ age: int = 0
64
+ active: bool = True
65
+
66
+ # Ou com T() explicito
67
+ # from pybase.validation import T
68
+ # db.schema.users = pb.Schema({"name": pb.T(str), "age": pb.T(int, default=0)})
69
+
70
+ # Colecao com validacao automatica
71
+ users = db["users"]
72
+ users.add({"name": "marcos", "email": "m@m.com"}) # auto-ID
65
73
 
66
74
  # SDK client
67
- doc = users.doc("1").get()
68
- users.where("age", "==", 30).get()
75
+ doc = users["1"].get()
76
+ users.find(age=0)
69
77
 
70
- # Consultas
71
- q = Query(users)
72
- q.sort("name").limit(10).find(active=True)
73
- q.find_one(name="ana")
74
- q.exists(name="joao")
78
+ # Consultas direto na colecao (v0.4+)
79
+ users.sort("name").limit(10).find(active=True)
80
+ users.find_one(name="ana")
81
+ users.exists(name="joao")
75
82
 
76
83
  # Servidor REST (requer Flask)
77
- Server(db).route("users").run(port=4560)
84
+ pb.Server(db).run(port=4560) # auto-detecta colecoes
78
85
  # Swagger UI: http://localhost:4560/mydb/docs/
79
86
  ```
80
87
 
@@ -83,48 +90,53 @@ Server(db).route("users").run(port=4560)
83
90
  | Modulo | Arquivo | Descricao | Deps |
84
91
  |--------|---------|-----------|------|
85
92
  | Database | `pybase/database.py` | Gerencia diretorio de tabelas JSON | 0 |
86
- | Collection | `pybase/collections.py` | CRUD atomico thread-safe | 0 |
87
- | Query | `pybase/query.py` | Consultas com sort/limit/offset | 0 |
88
- | Schema | `pybase/validation.py` | Validacao de documentos | 0 |
93
+ | Collection | `pybase/collections.py` | CRUD atomico thread-safe + query integrado | 0 |
94
+ | Query | `pybase/query.py` | Thin wrapper sobre Collection | 0 |
95
+ | Schema | `pybase/validation.py` | Validacao com T(), decorator, from_example | 0 |
89
96
  | Server | `pybase/server.py` | REST API automatica + Swagger UI | Flask |
90
97
  | Media | `pybase/media.py` | Upload/download de imagens | 0 |
91
- | Backup | `pybase/backup.py` | Snapshot e restore | 0 |
98
+ | Backup | `pybase/backup.py` | Snapshot e restore (seletivo) | 0 |
99
+ | Crypto | `pybase/crypto.py` | Criptografia stdlib (scrypt + XOR) | 0 |
100
+
101
+ ## Novidades da v0.4
102
+
103
+ - **API simplificada** — `Database("./data/mydb")` sem senha obrigatoria
104
+ - **`update`/`delete`** — nomes em ingles, sem aliases pt-br
105
+ - **Auto-ID** — `users.add({"name": "x"})` gera UUID automatico
106
+ - **`db["colecao"]`** — acesso direto via `__getitem__`
107
+ - **Query integrado** — `collection.sort().limit().find()` direto
108
+ - **`@db.schema.nome`** — schema via decorator de classe
109
+ - **`T()`** — nova API de tipos com kwargs (`T(int, default=0)`)
110
+ - **`Schema.from_example()`** — schema inferido de dados reais
111
+ - **`import global`** — `from pybase import pybase`
112
+ - **PBKDF2** — hashing de senha com salt (600k iteracoes)
113
+ - **Criptografia** — `encrypt()`/`decrypt()` stdlib puro
114
+ - **Server auto-detect** — `Server(db).run()` descobre colecoes
115
+ - **Erros descritivos** — com path + valor + sugestao
92
116
 
93
117
  ## Estrutura
94
118
 
95
119
  ```
96
120
  Pybase/
97
121
  pybase/
98
- __init__.py # Database, Collection, Query, Server, ...
99
- database.py # Database: init, open, collection, schema, drop
100
- collections.py # Collection, SchemaCollection, DocRef, QueryBuilder
101
- query.py # Query: find/find_one/exists + sort/limit/offset
102
- exceptions.py # PybaseError > DatabaseError, CollectionError
103
- validation.py # Schema, ValidationError
122
+ __init__.py # Database, Collection, Query, Server, Schema, T
123
+ pybase.py # from pybase import pybase
124
+ database.py # Database, SchemaRegistrar
125
+ collections.py # Collection, DocRef, QueryBuilder, SchemaCollection
126
+ query.py # Query wrapper
127
+ exceptions.py # PybaseError > DatabaseError, CollectionError, ...
128
+ validation.py # Schema, T, ValidationError
104
129
  server.py # Server REST (Flask)
105
130
  media.py # MediaManager
106
131
  index.py # Index (hash O(1))
107
132
  hooks.py # HooksCollection
108
133
  backup.py # take, restore
109
- utils.py # Utils: sha256 encode
134
+ crypto.py # encrypt, decrypt (stdlib)
135
+ utils.py # PBKDF2 encode/verify
110
136
  docs/ # Documentacao completa
111
137
  pyproject.toml
112
138
  ```
113
139
 
114
- ## Novidades da versao (0.3+)
115
-
116
- - **Schema validation** — defina schemas opcionais por colecao, persistidos em disco, validacao automatica em `add`/`add_many`/`actualize`
117
- - **Server REST** — `Server(db).route("x").run()` gera CRUD automatico com Swagger UI e OpenAPI spec
118
- - **MediaManager** — upload de imagens (path, bytes, base64), CRUD de metadados, serve files via HTTP
119
- - **SDK client** — `doc("id").get()`, `where("f", "==", "v").get()`
120
- - **SchemaCollection** — auto-wrap quando schema existe, validacao parcial em updates
121
- - **Backup** — `take()`/`restore()` com snapshot gzip atomico
122
- - **Escrita atomica** — `.tmp` + `os.replace()` protege contra corrupcao
123
- - **Path traversal fix** — nomes de colecao sanitizados com regex
124
- - **Sort seguro** — tipos mistos (int + str) nao quebram mais
125
- - **Indices em memoria** — `Index(field)` para lookups O(1)
126
- - **Lifecycle hooks** — `HooksCollection` com callbacks before/after
127
-
128
140
  ## Documentacao
129
141
 
130
142
  Veja [docs/](docs/index.md) com referencia completa de cada classe e metodo.
@@ -0,0 +1,120 @@
1
+ <p align="center">
2
+ <img src="assets/logo/logo.png" width="120" alt="Pybase logo">
3
+ </p>
4
+
5
+ # Pybase v0.4
6
+
7
+ Base para criacao de bancos de dados locais com JSON. Zero dependencias, 100% stdlib.
8
+ Operacoes de escrita atomicas, thread-safe.
9
+
10
+ Filosofia **Lego**: cada modulo e independente — voce monta o que precisa.
11
+
12
+ ## Instalacao
13
+
14
+ ```bash
15
+ pip install pybasedb-json
16
+ ```
17
+
18
+ Para o servidor REST (opcional):
19
+ ```bash
20
+ pip install Flask
21
+ ```
22
+
23
+ ## Uso rapido
24
+
25
+ ```python
26
+ from pybase import pybase as pb
27
+
28
+ # Banco de dados (sem senha ou com senha)
29
+ db = pb.Database("./data/mydb")
30
+ # db = pb.Database("./data/mydb", password="senha").open("senha")
31
+
32
+ # Schema com decorator (v0.4+)
33
+ @db.schema.users
34
+ class User:
35
+ name: str
36
+ email: str
37
+ age: int = 0
38
+ active: bool = True
39
+
40
+ # Ou com T() explicito
41
+ # from pybase.validation import T
42
+ # db.schema.users = pb.Schema({"name": pb.T(str), "age": pb.T(int, default=0)})
43
+
44
+ # Colecao com validacao automatica
45
+ users = db["users"]
46
+ users.add({"name": "marcos", "email": "m@m.com"}) # auto-ID
47
+
48
+ # SDK client
49
+ doc = users["1"].get()
50
+ users.find(age=0)
51
+
52
+ # Consultas direto na colecao (v0.4+)
53
+ users.sort("name").limit(10).find(active=True)
54
+ users.find_one(name="ana")
55
+ users.exists(name="joao")
56
+
57
+ # Servidor REST (requer Flask)
58
+ pb.Server(db).run(port=4560) # auto-detecta colecoes
59
+ # Swagger UI: http://localhost:4560/mydb/docs/
60
+ ```
61
+
62
+ ## Modulos
63
+
64
+ | Modulo | Arquivo | Descricao | Deps |
65
+ |--------|---------|-----------|------|
66
+ | Database | `pybase/database.py` | Gerencia diretorio de tabelas JSON | 0 |
67
+ | Collection | `pybase/collections.py` | CRUD atomico thread-safe + query integrado | 0 |
68
+ | Query | `pybase/query.py` | Thin wrapper sobre Collection | 0 |
69
+ | Schema | `pybase/validation.py` | Validacao com T(), decorator, from_example | 0 |
70
+ | Server | `pybase/server.py` | REST API automatica + Swagger UI | Flask |
71
+ | Media | `pybase/media.py` | Upload/download de imagens | 0 |
72
+ | Backup | `pybase/backup.py` | Snapshot e restore (seletivo) | 0 |
73
+ | Crypto | `pybase/crypto.py` | Criptografia stdlib (scrypt + XOR) | 0 |
74
+
75
+ ## Novidades da v0.4
76
+
77
+ - **API simplificada** — `Database("./data/mydb")` sem senha obrigatoria
78
+ - **`update`/`delete`** — nomes em ingles, sem aliases pt-br
79
+ - **Auto-ID** — `users.add({"name": "x"})` gera UUID automatico
80
+ - **`db["colecao"]`** — acesso direto via `__getitem__`
81
+ - **Query integrado** — `collection.sort().limit().find()` direto
82
+ - **`@db.schema.nome`** — schema via decorator de classe
83
+ - **`T()`** — nova API de tipos com kwargs (`T(int, default=0)`)
84
+ - **`Schema.from_example()`** — schema inferido de dados reais
85
+ - **`import global`** — `from pybase import pybase`
86
+ - **PBKDF2** — hashing de senha com salt (600k iteracoes)
87
+ - **Criptografia** — `encrypt()`/`decrypt()` stdlib puro
88
+ - **Server auto-detect** — `Server(db).run()` descobre colecoes
89
+ - **Erros descritivos** — com path + valor + sugestao
90
+
91
+ ## Estrutura
92
+
93
+ ```
94
+ Pybase/
95
+ pybase/
96
+ __init__.py # Database, Collection, Query, Server, Schema, T
97
+ pybase.py # from pybase import pybase
98
+ database.py # Database, SchemaRegistrar
99
+ collections.py # Collection, DocRef, QueryBuilder, SchemaCollection
100
+ query.py # Query wrapper
101
+ exceptions.py # PybaseError > DatabaseError, CollectionError, ...
102
+ validation.py # Schema, T, ValidationError
103
+ server.py # Server REST (Flask)
104
+ media.py # MediaManager
105
+ index.py # Index (hash O(1))
106
+ hooks.py # HooksCollection
107
+ backup.py # take, restore
108
+ crypto.py # encrypt, decrypt (stdlib)
109
+ utils.py # PBKDF2 encode/verify
110
+ docs/ # Documentacao completa
111
+ pyproject.toml
112
+ ```
113
+
114
+ ## Documentacao
115
+
116
+ Veja [docs/](docs/index.md) com referencia completa de cada classe e metodo.
117
+
118
+ ## Licenca
119
+
120
+ MIT
@@ -0,0 +1,44 @@
1
+ try:
2
+ from .server import Server
3
+ except ImportError:
4
+ Server = None
5
+
6
+ from .collections import Collection, DocRef, QueryBuilder, SchemaCollection
7
+ from .database import Database
8
+ from .exceptions import (
9
+ CollectionError,
10
+ DatabaseError,
11
+ DocumentNotFoundError,
12
+ EncryptionError,
13
+ MediaError,
14
+ PybaseError,
15
+ QueryError,
16
+ ValidationError,
17
+ )
18
+ from .query import Query
19
+
20
+ try:
21
+ from .validation import Schema, T
22
+ except ImportError:
23
+ Schema = None
24
+ T = None
25
+
26
+ __all__ = [
27
+ "Collection",
28
+ "Database",
29
+ "DocRef",
30
+ "DocumentNotFoundError",
31
+ "EncryptionError",
32
+ "MediaError",
33
+ "PybaseError",
34
+ "DatabaseError",
35
+ "CollectionError",
36
+ "Query",
37
+ "QueryBuilder",
38
+ "QueryError",
39
+ "Schema",
40
+ "SchemaCollection",
41
+ "Server",
42
+ "T",
43
+ "ValidationError",
44
+ ]
@@ -22,8 +22,8 @@ def take(
22
22
  dest: str | Path | None = None,
23
23
  *,
24
24
  compress: bool = True,
25
+ collections: list[str] | None = None,
25
26
  ) -> Path:
26
- # Snapshot every collection into one timestamped file
27
27
  backup_dir = Path(dest) if dest else (db.path_dir / _BACKUP_DIR)
28
28
  backup_dir.mkdir(parents=True, exist_ok=True)
29
29
 
@@ -31,8 +31,9 @@ def take(
31
31
  ext = ".json.gz" if compress else ".json"
32
32
  out_path = backup_dir / f"{db.name}_{ts}{ext}"
33
33
 
34
+ names = collections if collections else db.collections
34
35
  snapshot: dict[str, list[dict]] = {}
35
- for name in db.collections():
36
+ for name in names:
36
37
  snapshot[name] = db.collection(name).all()
37
38
 
38
39
  raw = json.dumps(snapshot, indent=2).encode("utf-8")
@@ -1,6 +1,8 @@
1
1
  import json
2
2
  import os
3
3
  import threading
4
+ import uuid
5
+ from collections.abc import Callable
4
6
  from pathlib import Path
5
7
 
6
8
  from .exceptions import CollectionError
@@ -10,6 +12,10 @@ class Collection:
10
12
  def __init__(self, file_path: str | Path) -> None:
11
13
  self.file_path = Path(file_path)
12
14
  self._lock = threading.Lock()
15
+ self._limit: int | None = None
16
+ self._offset: int | None = None
17
+ self._sort_key: str | None = None
18
+ self._sort_reverse: bool = False
13
19
 
14
20
  def __str__(self) -> str:
15
21
  return str(self.file_path)
@@ -20,8 +26,21 @@ class Collection:
20
26
  def __iter__(self):
21
27
  return iter(self._load())
22
28
 
23
- def __getitem__(self, index: int | slice) -> dict | list[dict]:
24
- return self._load()[index]
29
+ def __getitem__(self, key: str | int | slice) -> dict | list[dict] | None:
30
+ if isinstance(key, str):
31
+ return self.get(key)
32
+ return self._load()[key]
33
+
34
+ def __setitem__(self, doc_id: str, data: dict) -> None:
35
+ if not isinstance(doc_id, str):
36
+ raise TypeError("key must be a string document ID")
37
+ data = dict(data)
38
+ data["id"] = doc_id
39
+ existing = self.get(doc_id)
40
+ if existing:
41
+ self.update(data, id=doc_id)
42
+ else:
43
+ self.add(data)
25
44
 
26
45
  # -- internal I/O (unsafe -- caller must hold lock) --
27
46
 
@@ -40,7 +59,6 @@ class Collection:
40
59
  return data
41
60
 
42
61
  def _save_nolock(self, data: list) -> None:
43
- # write to .tmp then rename — atomic on POSIX / Windows
44
62
  tmp = self.file_path.with_suffix(".tmp")
45
63
  with open(tmp, "w", encoding="utf-8") as f:
46
64
  json.dump(data, f, indent=4)
@@ -65,15 +83,89 @@ class Collection:
65
83
  if all(doc.get(k) == v for k, v in match.items())
66
84
  )
67
85
 
68
- # .get() without args returns all docs; with args returns by id/key
69
- def get(self, doc_id=None, key="id"):
70
- if doc_id is None:
71
- return self.all()
86
+ def get(self, doc_id: str) -> dict | None:
72
87
  for doc in self._load():
73
- if doc.get(key) == doc_id:
88
+ if doc.get("id") == doc_id:
74
89
  return doc
75
90
  return None
76
91
 
92
+ # -- query methods (fluent, chained directly on Collection) --
93
+
94
+ def sort(self, key: str, reverse: bool = False) -> "Collection":
95
+ self._sort_key = key
96
+ self._sort_reverse = reverse
97
+ return self
98
+
99
+ def limit(self, n: int) -> "Collection":
100
+ self._limit = n
101
+ return self
102
+
103
+ def offset(self, n: int) -> "Collection":
104
+ self._offset = n
105
+ return self
106
+
107
+ def _make_sort_key(self) -> Callable[[dict], tuple]:
108
+ key = self._sort_key
109
+ type_order = {int: 0, float: 0, bool: 1, str: 2}
110
+ def _key(doc: dict) -> tuple:
111
+ val = doc.get(key)
112
+ if val is None:
113
+ return (1, 0, 0)
114
+ priority = type_order.get(type(val), 99)
115
+ return (0, priority, val)
116
+ return _key
117
+
118
+ def _apply(self, docs: list[dict]) -> list[dict]:
119
+ if self._sort_key:
120
+ docs = sorted(docs, key=self._make_sort_key(), reverse=self._sort_reverse)
121
+ if self._offset is not None:
122
+ docs = docs[self._offset:]
123
+ if self._limit is not None:
124
+ docs = docs[: self._limit]
125
+ return docs
126
+
127
+ def _reset_query(self) -> None:
128
+ self._limit = None
129
+ self._offset = None
130
+ self._sort_key = None
131
+ self._sort_reverse = False
132
+
133
+ def find(self, **match) -> list[dict]:
134
+ try:
135
+ docs = self.all()
136
+ if match:
137
+ docs = [
138
+ doc for doc in docs
139
+ if all(doc.get(k) == v for k, v in match.items())
140
+ ]
141
+ return self._apply(docs)
142
+ finally:
143
+ self._reset_query()
144
+
145
+ def find_one(self, **match) -> dict | None:
146
+ try:
147
+ docs = self.all()
148
+ if match:
149
+ docs = [
150
+ doc for doc in docs
151
+ if all(doc.get(k) == v for k, v in match.items())
152
+ ]
153
+ result = self._apply(docs)
154
+ return result[0] if result else None
155
+ finally:
156
+ self._reset_query()
157
+
158
+ def exists(self, **match) -> bool:
159
+ try:
160
+ if not match:
161
+ return len(self) > 0
162
+ for doc in self.all():
163
+ if all(doc.get(k) == v for k, v in match.items()):
164
+ return True
165
+ return False
166
+ finally:
167
+ self._reset_query()
168
+
77
169
  # -- SDK-style helpers --
78
170
 
79
171
  def doc(self, doc_id: str) -> "DocRef":
@@ -86,6 +178,8 @@ class Collection:
86
178
 
87
179
  def add(self, data: dict) -> dict:
88
180
  doc = dict(data)
181
+ if "id" not in doc:
182
+ doc["id"] = str(uuid.uuid4())
89
183
  with self._lock:
90
184
  docs = self._load_nolock()
91
185
  docs.append(doc)
@@ -97,34 +191,38 @@ class Collection:
97
191
  raise TypeError("add_many() expects a list of dicts")
98
192
  with self._lock:
99
193
  docs = self._load_nolock()
100
- added = [dict(item) for item in items]
194
+ added = []
195
+ for item in items:
196
+ doc = dict(item)
197
+ if "id" not in doc:
198
+ doc["id"] = str(uuid.uuid4())
199
+ added.append(doc)
101
200
  docs.extend(added)
102
201
  self._save_nolock(docs)
103
202
  return added
104
203
 
105
- def actualize(self, data: dict, **match) -> int:
204
+ def update(self, data: dict, **match) -> int:
106
205
  if not match:
107
206
  raise CollectionError(
108
207
  "provide at least one filter field, "
109
- "e.g. actualize({'age': 20}, name='marcos')"
208
+ "e.g. update({'age': 20}, name='marcos')"
110
209
  )
111
210
  with self._lock:
112
211
  docs = self._load_nolock()
113
212
  updated = 0
114
213
  for i, doc in enumerate(docs):
115
214
  if all(doc.get(k) == v for k, v in match.items()):
116
- # defensive copy — don't mutate original dicts in memory
117
215
  docs[i] = {**doc, **data}
118
216
  updated += 1
119
217
  if updated:
120
218
  self._save_nolock(docs)
121
219
  return updated
122
220
 
123
- def rem(self, **match) -> int:
221
+ def delete(self, **match) -> int:
124
222
  if not match:
125
223
  raise CollectionError(
126
224
  "provide at least one filter field, "
127
- "e.g. rem(name='marcos')"
225
+ "e.g. delete(name='marcos')"
128
226
  )
129
227
  with self._lock:
130
228
  docs = self._load_nolock()
@@ -152,10 +250,10 @@ class DocRef:
152
250
  return self._col.get(self._id)
153
251
 
154
252
  def update(self, data: dict) -> int:
155
- return self._col.actualize(data, id=self._id)
253
+ return self._col.update(data, id=self._id)
156
254
 
157
255
  def delete(self) -> int:
158
- return self._col.rem(id=self._id)
256
+ return self._col.delete(id=self._id)
159
257
 
160
258
 
161
259
  class QueryBuilder:
@@ -177,18 +275,13 @@ class QueryBuilder:
177
275
 
178
276
 
179
277
  class SchemaCollection:
180
- # Wraps a Collection + optional Schema.
181
- # Validates all writes against the schema before delegating.
182
-
183
278
  def __init__(self, collection: Collection, schema=None) -> None:
184
279
  self._col = collection
185
- self._schema = schema # None = passthrough, no validation
280
+ self._schema = schema
186
281
 
187
282
  def set_schema(self, schema) -> None:
188
283
  self._schema = schema
189
284
 
190
- # -- schema-aware writes --
191
-
192
285
  def add(self, data: dict) -> dict:
193
286
  doc = self._schema.validate(data) if self._schema else dict(data)
194
287
  return self._col.add(doc)
@@ -198,12 +291,10 @@ class SchemaCollection:
198
291
  items = [self._schema.validate(d) for d in items]
199
292
  return self._col.add_many(items)
200
293
 
201
- def actualize(self, data: dict, **match) -> int:
294
+ def update(self, data: dict, **match) -> int:
202
295
  if self._schema:
203
- self._schema.validate(data, partial=True) # partial update — only check types
204
- return self._col.actualize(data, **match)
205
-
206
- # -- delegation (reads + rem + drop + SDK helpers) --
296
+ self._schema.validate(data, partial=True)
297
+ return self._col.update(data, **match)
207
298
 
208
299
  def __getattr__(self, name: str):
209
300
  return getattr(self._col, name)
@@ -0,0 +1,59 @@
1
+ import base64
2
+ import hashlib
3
+ import os
4
+
5
+ from .exceptions import EncryptionError
6
+
7
+
8
+ def _derive_key(password: str, salt: bytes, key_len: int = 32) -> bytes:
9
+ return hashlib.scrypt(
10
+ password.encode("utf-8"),
11
+ salt=salt,
12
+ n=16384,
13
+ r=8,
14
+ p=1,
15
+ dklen=key_len,
16
+ )
17
+
18
+
19
+ def encrypt(data: str | bytes, password: str) -> str:
20
+ if isinstance(data, str):
21
+ data = data.encode("utf-8")
22
+ salt = os.urandom(16)
23
+ key = _derive_key(password, salt, len(data))
24
+ encrypted = bytes(a ^ b for a, b in zip(data, key))
25
+ payload = salt + encrypted
26
+ return base64.urlsafe_b64encode(payload).decode("ascii")
27
+
28
+
29
+ def decrypt(payload: str, password: str) -> str:
30
+ try:
31
+ raw = base64.urlsafe_b64decode(payload.encode("ascii"))
32
+ except Exception as e:
33
+ raise EncryptionError(f"invalid encrypted payload: {e}")
34
+ if len(raw) < 16:
35
+ raise EncryptionError("payload too short")
36
+ salt = raw[:16]
37
+ encrypted = raw[16:]
38
+ key = _derive_key(password, salt, len(encrypted))
39
+ decrypted = bytes(a ^ b for a, b in zip(encrypted, key))
40
+ try:
41
+ return decrypted.decode("utf-8")
42
+ except UnicodeDecodeError:
43
+ raise EncryptionError("incorrect password or corrupted data")
44
+
45
+
46
+ def encrypt_bytes(data: bytes, password: str) -> bytes:
47
+ salt = os.urandom(16)
48
+ key = _derive_key(password, salt, len(data))
49
+ encrypted = bytes(a ^ b for a, b in zip(data, key))
50
+ return salt + encrypted
51
+
52
+
53
+ def decrypt_bytes(payload: bytes, password: str) -> bytes:
54
+ if len(payload) < 16:
55
+ raise EncryptionError("payload too short")
56
+ salt = payload[:16]
57
+ encrypted = payload[16:]
58
+ key = _derive_key(password, salt, len(encrypted))
59
+ return bytes(a ^ b for a, b in zip(encrypted, key))