vrbase 1.0.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.
- vrbase-1.0.0.dist-info/METADATA +624 -0
- vrbase-1.0.0.dist-info/RECORD +5 -0
- vrbase-1.0.0.dist-info/WHEEL +5 -0
- vrbase-1.0.0.dist-info/top_level.txt +1 -0
- vrbase.py +512 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vrbase
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Русская документо-ориентированная база данных — проще чем SQL
|
|
5
|
+
Author: Pantapan1
|
|
6
|
+
Keywords: база данных,database,json,русский,documents,nosql
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: cryptography>=41.0.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: keywords
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
VRBase — Русская база данных
|
|
27
|
+
|
|
28
|
+
Документо-ориентированная база данных на русском языке. Проще чем SQL, быстрее чем JSON вручную.
|
|
29
|
+
|
|
30
|
+
https://img.shields.io/pypi/v/vrbase.svg
|
|
31
|
+
https://img.shields.io/badge/python-3.8+-blue.svg
|
|
32
|
+
https://img.shields.io/badge/license-MIT-green.svg
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
📖 Оглавление
|
|
37
|
+
|
|
38
|
+
· Почему VRBase
|
|
39
|
+
· Установка
|
|
40
|
+
· Быстрый старт
|
|
41
|
+
· Основные понятия
|
|
42
|
+
· Полное руководство
|
|
43
|
+
· База данных
|
|
44
|
+
· Коробка
|
|
45
|
+
· Документы
|
|
46
|
+
· Поиск
|
|
47
|
+
· Обновление
|
|
48
|
+
· Удаление
|
|
49
|
+
· Индексы
|
|
50
|
+
· Запросы
|
|
51
|
+
· Агрегация
|
|
52
|
+
· Шифрование
|
|
53
|
+
· Миграции
|
|
54
|
+
· Сравнение с SQL
|
|
55
|
+
· Примеры
|
|
56
|
+
· API Reference
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
Почему VRBase
|
|
61
|
+
|
|
62
|
+
Проблема SQL
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
CREATE TABLE players (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
name TEXT NOT NULL,
|
|
68
|
+
level INTEGER DEFAULT 1,
|
|
69
|
+
inventory TEXT,
|
|
70
|
+
stats TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
INSERT INTO players (name, level, inventory, stats)
|
|
74
|
+
VALUES ('Миро', 10, '["меч","щит"]', '{"hp":100,"mp":50}');
|
|
75
|
+
|
|
76
|
+
SELECT * FROM players WHERE level > 5 ORDER BY level DESC LIMIT 10;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Решение VRBase
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from vrbase import База
|
|
83
|
+
|
|
84
|
+
мир = База("survival")
|
|
85
|
+
игроки = мир.коробка("игроки")
|
|
86
|
+
|
|
87
|
+
игроки.положить({
|
|
88
|
+
"имя": "Миро",
|
|
89
|
+
"уровень": 10,
|
|
90
|
+
"инвентарь": ["меч", "щит"],
|
|
91
|
+
"статы": {"здоровье": 100, "мана": 50}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
топ = игроки.найти(где={"уровень": "> 5"}, сортировка="уровень убыв", лимит=10)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Разница: Не нужно думать о схемах, типах данных, JOIN'ах, миграциях. Просто кладёшь словарь и ищешь по-русски.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
Установка
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install vrbase
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Или из исходников:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/Pantapan1/VRBase.git
|
|
111
|
+
cd VRBase
|
|
112
|
+
pip install -e .
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Зависимости: cryptography>=41.0.0 (только для шифрования)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
Быстрый старт
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from vrbase import База
|
|
123
|
+
|
|
124
|
+
# Создаём или открываем базу
|
|
125
|
+
мир = База("мой_мир")
|
|
126
|
+
|
|
127
|
+
# Создаём коробку (аналог таблицы)
|
|
128
|
+
игроки = мир.коробка("игроки")
|
|
129
|
+
|
|
130
|
+
# Добавляем игрока
|
|
131
|
+
игроки.положить({
|
|
132
|
+
"имя": "Миро",
|
|
133
|
+
"уровень": 1,
|
|
134
|
+
"класс": "воин",
|
|
135
|
+
"инвентарь": ["меч", "зелье"],
|
|
136
|
+
"статы": {"здоровье": 100, "мана": 50}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Ищем
|
|
140
|
+
новички = игроки.найти(где={"уровень": "< 5"})
|
|
141
|
+
|
|
142
|
+
# Обновляем
|
|
143
|
+
игроки.обновить(где={"имя": "Миро"}, данные={"уровень": 10})
|
|
144
|
+
|
|
145
|
+
# Сохраняем
|
|
146
|
+
мир.сохранить()
|
|
147
|
+
|
|
148
|
+
print(f"Игроков: {игроки.считать()}")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
Основные понятия
|
|
154
|
+
|
|
155
|
+
VRBase SQL
|
|
156
|
+
База Database
|
|
157
|
+
Коробка Table
|
|
158
|
+
Документ Row
|
|
159
|
+
Ключ Column
|
|
160
|
+
положить() INSERT
|
|
161
|
+
найти() SELECT
|
|
162
|
+
обновить() UPDATE
|
|
163
|
+
удалить() DELETE
|
|
164
|
+
Индекс Index
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
Полное руководство
|
|
169
|
+
|
|
170
|
+
База данных
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from vrbase import База
|
|
174
|
+
|
|
175
|
+
# Обычная база (файл .json)
|
|
176
|
+
мир = База("survival")
|
|
177
|
+
|
|
178
|
+
# База с шифрованием (.enc)
|
|
179
|
+
мир = База("survival", ключ="мой_секретный_пароль")
|
|
180
|
+
|
|
181
|
+
# База в конкретной папке
|
|
182
|
+
мир = База("survival", путь="/path/to/survival.json")
|
|
183
|
+
|
|
184
|
+
# Контекстный менеджер (авто-сохранение)
|
|
185
|
+
with База("survival") as мир:
|
|
186
|
+
игроки = мир.коробка("игроки")
|
|
187
|
+
игроки.положить({"имя": "Миро"})
|
|
188
|
+
# Автоматически сохранится
|
|
189
|
+
|
|
190
|
+
# Просмотр информации
|
|
191
|
+
print(мир.инфо())
|
|
192
|
+
# {'имя': 'survival', 'коробок': 1, 'документов': 1, 'размер_байт': 256}
|
|
193
|
+
|
|
194
|
+
# Бэкап
|
|
195
|
+
мир.бэкап() # → survival_2026-05-09_12-30-00.json
|
|
196
|
+
|
|
197
|
+
# Сохранить копию
|
|
198
|
+
мир.сохранить_как("survival_backup.json")
|
|
199
|
+
|
|
200
|
+
# Очистить
|
|
201
|
+
мир.очистить() # Удаляет всё
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Коробка
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
игроки = мир.коробка("игроки")
|
|
208
|
+
|
|
209
|
+
# Сколько документов
|
|
210
|
+
print(len(игроки)) # 5
|
|
211
|
+
print(игроки.размер()) # 5
|
|
212
|
+
print(игроки.память()) # 2500 (байт в памяти)
|
|
213
|
+
|
|
214
|
+
# Все ключи документов
|
|
215
|
+
print(игроки.ключи()) # ['_id', '_создан', 'имя', 'уровень', ...]
|
|
216
|
+
|
|
217
|
+
# Уникальные значения поля
|
|
218
|
+
print(игроки.уникальные("класс")) # ['воин', 'маг', 'вор']
|
|
219
|
+
|
|
220
|
+
# Есть ли документ с таким условием
|
|
221
|
+
if игроки.есть(где={"имя": "Миро"}):
|
|
222
|
+
print("Миро существует")
|
|
223
|
+
|
|
224
|
+
# Перебор
|
|
225
|
+
for игрок in игроки:
|
|
226
|
+
print(игрок['имя'])
|
|
227
|
+
|
|
228
|
+
# Доступ по ID
|
|
229
|
+
игрок = игроки[0] # Первый документ
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Документы
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
# Добавить один
|
|
236
|
+
ид = игроки.положить({
|
|
237
|
+
"имя": "Миро",
|
|
238
|
+
"уровень": 1,
|
|
239
|
+
"инвентарь": ["меч", "хлеб"],
|
|
240
|
+
"статы": {"здоровье": 100, "выносливость": 80}
|
|
241
|
+
})
|
|
242
|
+
print(ид) # 0
|
|
243
|
+
|
|
244
|
+
# Добавить много
|
|
245
|
+
игроки.положить_много([
|
|
246
|
+
{"имя": "Кай", "уровень": 5},
|
|
247
|
+
{"имя": "Анна", "уровень": 7},
|
|
248
|
+
{"имя": "Олег", "уровень": 3}
|
|
249
|
+
])
|
|
250
|
+
|
|
251
|
+
# Автоматически добавляются поля:
|
|
252
|
+
# _id — уникальный номер
|
|
253
|
+
# _создан — время создания в ISO формате
|
|
254
|
+
# _обновлён — время последнего обновления
|
|
255
|
+
|
|
256
|
+
игрок = игроки.найти_один(где={"имя": "Миро"})
|
|
257
|
+
print(игрок['_id']) # 0
|
|
258
|
+
print(игрок['_создан']) # 2026-05-09T12:30:00
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Поиск
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
# Точное совпадение
|
|
265
|
+
игроки.найти(где={"уровень": 10})
|
|
266
|
+
|
|
267
|
+
# Больше / меньше
|
|
268
|
+
игроки.найти(где={"уровень": "> 5"})
|
|
269
|
+
игроки.найти(где={"уровень": ">= 10"})
|
|
270
|
+
игроки.найти(где={"уровень": "< 3"})
|
|
271
|
+
игроки.найти(где={"уровень": "!= 5"})
|
|
272
|
+
|
|
273
|
+
# Диапазон
|
|
274
|
+
игроки.найти(где={"уровень": "между 5 и 20"})
|
|
275
|
+
|
|
276
|
+
# Поиск подстроки
|
|
277
|
+
игроки.найти(где={"имя": "содержит мир"}) # найдёт "Миро", "Владимир"
|
|
278
|
+
|
|
279
|
+
# Начинается с / заканчивается на
|
|
280
|
+
игроки.найти(где={"имя": "начинается с М"})
|
|
281
|
+
игроки.найти(где={"имя": "заканчивается на о"})
|
|
282
|
+
|
|
283
|
+
# Пусто / не пусто
|
|
284
|
+
игроки.найти(где={"инвентарь": "пусто"})
|
|
285
|
+
игроки.найти(где={"инвентарь": "не пусто"})
|
|
286
|
+
|
|
287
|
+
# Поиск по вложенному полю
|
|
288
|
+
игроки.найти(где={"статы.здоровье": "> 50"})
|
|
289
|
+
|
|
290
|
+
# Содержит в списке
|
|
291
|
+
игроки.найти(содержит={"инвентарь": "меч"})
|
|
292
|
+
|
|
293
|
+
# Сортировка
|
|
294
|
+
игроки.найти(сортировка="уровень убыв")
|
|
295
|
+
игроки.найти(сортировка="имя возр")
|
|
296
|
+
|
|
297
|
+
# Комбинированный поиск
|
|
298
|
+
ветераны = игроки.найти(
|
|
299
|
+
где={"уровень": "> 10", "класс": "воин"},
|
|
300
|
+
сортировка="уровень убыв",
|
|
301
|
+
лимит=5
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Найти один
|
|
305
|
+
миро = игроки.найти_один(имя="Миро")
|
|
306
|
+
|
|
307
|
+
# Найти по ID
|
|
308
|
+
игрок = игроки.найти_по_id(0)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Обновление
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
# Обновить по условию
|
|
315
|
+
игроки.обновить(
|
|
316
|
+
где={"имя": "Миро"},
|
|
317
|
+
данные={"уровень": 15, "золото": 500}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Обновить по ID
|
|
321
|
+
игроки.обновить_по_id(0, {"уровень": 20})
|
|
322
|
+
|
|
323
|
+
# Обновить все документы
|
|
324
|
+
игроки.обновить_все({"бонус": 0})
|
|
325
|
+
|
|
326
|
+
# Обновить вложенное поле
|
|
327
|
+
игроки.обновить(
|
|
328
|
+
где={"имя": "Миро"},
|
|
329
|
+
данные={"статы.здоровье": 150}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Авто-обновление _обновлён
|
|
333
|
+
игрок = игроки.найти_один(имя="Миро")
|
|
334
|
+
print(игрок['_обновлён']) # 2026-05-09T13:00:00
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Удаление
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
# Удалить по условию
|
|
341
|
+
игроки.удалить(где={"уровень": "< 2"})
|
|
342
|
+
|
|
343
|
+
# Удалить по ID
|
|
344
|
+
игроки.удалить_по_id(0)
|
|
345
|
+
|
|
346
|
+
# Удалить всё
|
|
347
|
+
игроки.удалить_все()
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Индексы
|
|
351
|
+
|
|
352
|
+
Индексы ускоряют поиск в 10-1000 раз на больших объёмах данных.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
# Создать индекс
|
|
356
|
+
игроки.создать_индекс("имя")
|
|
357
|
+
игроки.создать_индекс("уровень")
|
|
358
|
+
|
|
359
|
+
# Составной индекс
|
|
360
|
+
игроки.создать_индекс(["класс", "уровень"])
|
|
361
|
+
|
|
362
|
+
# Поиск с индексом (автоматически используется)
|
|
363
|
+
игроки.найти(где={"имя": "Миро"}) # O(log n) вместо O(n)
|
|
364
|
+
|
|
365
|
+
# Удалить индекс
|
|
366
|
+
игроки.удалить_индекс("имя")
|
|
367
|
+
|
|
368
|
+
# Перестроить все индексы
|
|
369
|
+
игроки.перестроить_индексы()
|
|
370
|
+
|
|
371
|
+
# Статистика индекса
|
|
372
|
+
индекс = игроки.создать_индекс("уровень")
|
|
373
|
+
print(индекс.статистика())
|
|
374
|
+
# {'тип': 'числовой', 'уникальных': 15, 'минимум': 1, 'максимум': 50, 'среднее': 12.3}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Запросы
|
|
378
|
+
|
|
379
|
+
Сложные запросы через конструктор:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from vrbase import Запрос
|
|
383
|
+
|
|
384
|
+
# Сложный запрос
|
|
385
|
+
результат = (Запрос(игроки)
|
|
386
|
+
.где(уровень="> 5")
|
|
387
|
+
.и_(класс="воин")
|
|
388
|
+
.или(класс="маг")
|
|
389
|
+
.не_(статус="мёртв")
|
|
390
|
+
.содержит(инвентарь="меч")
|
|
391
|
+
.сортировать_по("уровень", убывание=True)
|
|
392
|
+
.лимит(10)
|
|
393
|
+
.пропустить(5)
|
|
394
|
+
.выполнить())
|
|
395
|
+
|
|
396
|
+
# Выбрать только определённые поля
|
|
397
|
+
результат = (Запрос(игроки)
|
|
398
|
+
.выбрать("имя", "уровень")
|
|
399
|
+
.где(уровень="> 10")
|
|
400
|
+
.выполнить())
|
|
401
|
+
|
|
402
|
+
# Уникальные значения
|
|
403
|
+
результат = (Запрос(игроки)
|
|
404
|
+
.выбрать("класс")
|
|
405
|
+
.уникальные()
|
|
406
|
+
.выполнить())
|
|
407
|
+
|
|
408
|
+
# Проверить существование
|
|
409
|
+
есть = (Запрос(игроки)
|
|
410
|
+
.где(имя="Миро")
|
|
411
|
+
.существует())
|
|
412
|
+
|
|
413
|
+
# Парсинг строки запроса
|
|
414
|
+
from vrbase import ПарсерЗапроса
|
|
415
|
+
|
|
416
|
+
запрос = ПарсерЗапроса.разобрать(
|
|
417
|
+
"выбрать имя, уровень где уровень > 5 сортировка по уровень лимит 5",
|
|
418
|
+
игроки
|
|
419
|
+
)
|
|
420
|
+
результат = запрос.выполнить()
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Агрегация
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
# Подсчёт
|
|
427
|
+
стата = (игроки.агрегация()
|
|
428
|
+
.считать()
|
|
429
|
+
.выполнить())
|
|
430
|
+
# [{'количество': 150}]
|
|
431
|
+
|
|
432
|
+
# Группировка
|
|
433
|
+
стата = (игроки.агрегация()
|
|
434
|
+
.группировать_по("класс")
|
|
435
|
+
.считать()
|
|
436
|
+
.среднее("уровень")
|
|
437
|
+
.выполнить())
|
|
438
|
+
# [
|
|
439
|
+
# {'класс': 'воин', 'количество': 50, 'среднее_уровень': 12.3},
|
|
440
|
+
# {'класс': 'маг', 'количество': 30, 'среднее_уровень': 15.1}
|
|
441
|
+
# ]
|
|
442
|
+
|
|
443
|
+
# Сумма, минимум, максимум
|
|
444
|
+
стата = (игроки.агрегация()
|
|
445
|
+
.группировать_по("класс")
|
|
446
|
+
.считать("игроков")
|
|
447
|
+
.сумма("золото", "всего_золота")
|
|
448
|
+
.максимум("уровень", "макс_уровень")
|
|
449
|
+
.выполнить())
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Шифрование
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from vrbase import Шифр
|
|
456
|
+
|
|
457
|
+
# Зашифровать данные
|
|
458
|
+
зашифровано = Шифр.зашифровать("секретные данные", "мой_пароль")
|
|
459
|
+
|
|
460
|
+
# Расшифровать
|
|
461
|
+
данные = Шифр.расшифровать(зашифровано, "мой_пароль")
|
|
462
|
+
|
|
463
|
+
# Зашифровать файл
|
|
464
|
+
Шифр.зашифровать_файл("data.json", "пароль", "data.enc")
|
|
465
|
+
|
|
466
|
+
# Расшифровать файл
|
|
467
|
+
Шифр.расшифровать_файл("data.enc", "пароль", "data.json")
|
|
468
|
+
|
|
469
|
+
# Хэш строки
|
|
470
|
+
хэш = Шифр.хэш("важные данные")
|
|
471
|
+
|
|
472
|
+
# Хэш файла
|
|
473
|
+
хэш = Шифр.хэш_файла("data.json")
|
|
474
|
+
|
|
475
|
+
# Сравнить файлы
|
|
476
|
+
одинаковые = Шифр.сравнить_хэши("file1.json", "file2.json")
|
|
477
|
+
|
|
478
|
+
# Электронная подпись
|
|
479
|
+
подпись = Шифр.подписать("документ", "ключ")
|
|
480
|
+
валидна = Шифр.проверить_подпись("документ", подпись, "ключ")
|
|
481
|
+
|
|
482
|
+
# Сгенерировать пароль
|
|
483
|
+
пароль = Шифр.сгенерировать_пароль(16)
|
|
484
|
+
|
|
485
|
+
# Проверить надёжность пароля
|
|
486
|
+
print(Шифр.надёжность_пароля("qwerty")) # Слабый
|
|
487
|
+
print(Шифр.надёжность_пароля("Qw3rty!@2026")) # Очень надёжный
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Миграции
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from vrbase import Миграция, МенеджерМиграций
|
|
494
|
+
|
|
495
|
+
менеджер = МенеджерМиграций(мир)
|
|
496
|
+
|
|
497
|
+
# Создать миграцию
|
|
498
|
+
v1 = (Миграция("1.0.0", "Начальная структура")
|
|
499
|
+
.добавить_поле("игроки", "золото", 100)
|
|
500
|
+
.добавить_поле("игроки", "опыт", 0))
|
|
501
|
+
|
|
502
|
+
v2 = (Миграция("1.1.0", "Добавляем магию")
|
|
503
|
+
.добавить_поле("игроки", "мана", 50)
|
|
504
|
+
.добавить_поле("игроки", "макс_мана", 50))
|
|
505
|
+
|
|
506
|
+
v3 = (Миграция("2.0.0", "Переименовываем")
|
|
507
|
+
.переименовать_поле("игроки", "золото", "монеты"))
|
|
508
|
+
|
|
509
|
+
# Зарегистрировать
|
|
510
|
+
менеджер.зарегистрировать(v1)
|
|
511
|
+
менеджер.зарегистрировать(v2)
|
|
512
|
+
менеджер.зарегистрировать(v3)
|
|
513
|
+
|
|
514
|
+
# Применить все
|
|
515
|
+
менеджер.применить()
|
|
516
|
+
|
|
517
|
+
# Применить до определённой версии
|
|
518
|
+
менеджер.применить(до_версии="1.1.0")
|
|
519
|
+
|
|
520
|
+
# Статус
|
|
521
|
+
print(менеджер.статус())
|
|
522
|
+
# {'всего': 3, 'применено': 2, 'ожидают': 1}
|
|
523
|
+
|
|
524
|
+
# Откатить
|
|
525
|
+
менеджер.откатить("1.0.0")
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
Сравнение с SQL
|
|
531
|
+
|
|
532
|
+
Действие SQL VRBase
|
|
533
|
+
Создать таблицу CREATE TABLE ... мир.коробка("имя")
|
|
534
|
+
Вставить INSERT INTO ... .положить({...})
|
|
535
|
+
Выбрать все SELECT * FROM ... .найти_все()
|
|
536
|
+
Выбрать с условием WHERE x = 5 .найти(где={"x": 5})
|
|
537
|
+
Больше/меньше WHERE x > 5 .найти(где={"x": "> 5"})
|
|
538
|
+
Сортировка ORDER BY x DESC сортировка="x убыв"
|
|
539
|
+
Лимит LIMIT 10 лимит=10
|
|
540
|
+
Обновить UPDATE ... SET ... .обновить(где=..., данные=...)
|
|
541
|
+
Удалить DELETE FROM ... .удалить(где=...)
|
|
542
|
+
Индекс CREATE INDEX ... .создать_индекс(...)
|
|
543
|
+
JOIN LEFT JOIN ... Вложенные документы
|
|
544
|
+
Схема Обязательна Не нужна
|
|
545
|
+
Миграции ALTER TABLE ... Автоматически
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
Примеры
|
|
550
|
+
|
|
551
|
+
RPG-бот на RuGram + VRBase
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
from ядро import РуГрам
|
|
555
|
+
from vrbase import База
|
|
556
|
+
|
|
557
|
+
бот = РуГрам("ТОКЕН")
|
|
558
|
+
мир = База("rpg_bot")
|
|
559
|
+
игроки = мир.коробка("игроки")
|
|
560
|
+
|
|
561
|
+
@бот.обработать(команда="start")
|
|
562
|
+
def старт(сообщение, контекст):
|
|
563
|
+
игрок = игроки.найти_один(чат_ид=сообщение.чат.ид)
|
|
564
|
+
if not игрок:
|
|
565
|
+
игроки.положить({
|
|
566
|
+
"чат_ид": сообщение.чат.ид,
|
|
567
|
+
"имя": сообщение.от.имя,
|
|
568
|
+
"уровень": 1,
|
|
569
|
+
"здоровье": 100,
|
|
570
|
+
"золото": 0
|
|
571
|
+
})
|
|
572
|
+
мир.сохранить()
|
|
573
|
+
|
|
574
|
+
бот.отправить(сообщение.чат.ид, "Добро пожаловать!")
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
API Reference
|
|
580
|
+
|
|
581
|
+
База
|
|
582
|
+
|
|
583
|
+
Метод Описание
|
|
584
|
+
База(имя, путь, ключ) Создать/открыть базу
|
|
585
|
+
.коробка(имя) Получить коробку
|
|
586
|
+
.сохранить() Сохранить на диск
|
|
587
|
+
.загрузить() Загрузить с диска
|
|
588
|
+
.бэкап() Создать копию
|
|
589
|
+
.очистить() Удалить всё
|
|
590
|
+
.инфо() Информация о базе
|
|
591
|
+
.список_коробок() Список коробок
|
|
592
|
+
.размер() Размер файла в байтах
|
|
593
|
+
|
|
594
|
+
Коробка
|
|
595
|
+
|
|
596
|
+
Метод Описание
|
|
597
|
+
.положить(документ) Добавить документ
|
|
598
|
+
.положить_много(список) Добавить много
|
|
599
|
+
.найти(где, содержит, сортировка, лимит) Поиск
|
|
600
|
+
.найти_один(**условия) Найти один
|
|
601
|
+
.найти_все() Все документы
|
|
602
|
+
.найти_по_id(ид) По ID
|
|
603
|
+
.считать(где) Количество
|
|
604
|
+
.обновить(где, данные) Обновить
|
|
605
|
+
.обновить_по_id(ид, данные) Обновить по ID
|
|
606
|
+
.обновить_все(данные) Обновить все
|
|
607
|
+
.удалить(где) Удалить
|
|
608
|
+
.удалить_по_id(ид) Удалить по ID
|
|
609
|
+
.удалить_все() Очистить
|
|
610
|
+
.есть(где) Проверить существование
|
|
611
|
+
.создать_индекс(ключ) Создать индекс
|
|
612
|
+
.удалить_индекс(ключ) Удалить индекс
|
|
613
|
+
.уникальные(ключ) Уникальные значения
|
|
614
|
+
.ключи() Все ключи
|
|
615
|
+
.экспорт() Экспорт данных
|
|
616
|
+
.импорт(данные) Импорт данных
|
|
617
|
+
.запрос() Создать запрос
|
|
618
|
+
.агрегация() Создать агрегацию
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
Лицензия
|
|
623
|
+
|
|
624
|
+
MIT License. Свободное использование, модификация и распространение.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
vrbase.py,sha256=f_sk8g6AcdNc8lE24Cfueq6LprbcCAITHEf96rO_mZM,22261
|
|
2
|
+
vrbase-1.0.0.dist-info/METADATA,sha256=N5haX_Co1c336K5AHQ0XMBl2FUoRPk1xyD0_RwcZf6c,19814
|
|
3
|
+
vrbase-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
vrbase-1.0.0.dist-info/top_level.txt,sha256=ph9YrF-dnBRXnRJ0gALZyonsUvVlLNoXnh3s_PW4oaI,7
|
|
5
|
+
vrbase-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vrbase
|
vrbase.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import hashlib
|
|
5
|
+
import base64
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, List, Dict, Optional, Union
|
|
8
|
+
from cryptography.fernet import Fernet
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ОшибкаБазы(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ОшибкаКоробки(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ОшибкаЗапроса(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ОшибкаШифрования(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Шифр:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def создать_ключ(пароль: str) -> bytes:
|
|
30
|
+
хэш = hashlib.sha256(пароль.encode()).digest()
|
|
31
|
+
return base64.urlsafe_b64encode(хэш[:32])
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def зашифровать(данные: str, пароль: str) -> bytes:
|
|
35
|
+
ключ = Шифр.создать_ключ(пароль)
|
|
36
|
+
фернет = Fernet(ключ)
|
|
37
|
+
return фернет.encrypt(данные.encode())
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def расшифровать(данные: bytes, пароль: str) -> str:
|
|
41
|
+
ключ = Шифр.создать_ключ(пароль)
|
|
42
|
+
фернет = Fernet(ключ)
|
|
43
|
+
return фернет.decrypt(данные).decode()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Условие:
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self.условия_и = []
|
|
49
|
+
self.условия_или = []
|
|
50
|
+
self.условия_не = []
|
|
51
|
+
self.условия_содержит = []
|
|
52
|
+
self.сортировка = None
|
|
53
|
+
self.обратная_сортировка = False
|
|
54
|
+
self.лимит_значение = None
|
|
55
|
+
self.пропустить_значение = 0
|
|
56
|
+
|
|
57
|
+
def где(self, **условия):
|
|
58
|
+
for ключ, значение in условия.items():
|
|
59
|
+
self.условия_и.append((ключ, значение))
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def и_(self, **условия):
|
|
63
|
+
return self.где(**условия)
|
|
64
|
+
|
|
65
|
+
def или(self, **условия):
|
|
66
|
+
for ключ, значение in условия.items():
|
|
67
|
+
self.условия_или.append((ключ, значение))
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def не_(self, **условия):
|
|
71
|
+
for ключ, значение in условия.items():
|
|
72
|
+
self.условия_не.append((ключ, значение))
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def содержит(self, **условия):
|
|
76
|
+
for ключ, значение in условия.items():
|
|
77
|
+
self.условия_содержит.append((ключ, значение))
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def сортировать_по(self, ключ: str, убывание: bool = False):
|
|
81
|
+
self.сортировка = ключ
|
|
82
|
+
self.обратная_сортировка = убывание
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def лимит(self, n: int):
|
|
86
|
+
self.лимит_значение = n
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def пропустить(self, n: int):
|
|
90
|
+
self.пропустить_значение = n
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def обратно(self):
|
|
94
|
+
self.обратная_сортировка = not self.обратная_сортировка
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Индекс:
|
|
99
|
+
def __init__(self, имя: str, ключ: Union[str, List[str]]):
|
|
100
|
+
self.имя = имя
|
|
101
|
+
self.ключи = [ключ] if isinstance(ключ, str) else ключ
|
|
102
|
+
self.данные = {}
|
|
103
|
+
|
|
104
|
+
def добавить(self, ид_документа: int, документ: Dict):
|
|
105
|
+
значения = tuple(self.получить_значение(документ, к) for к in self.ключи)
|
|
106
|
+
if значения not in self.данные:
|
|
107
|
+
self.данные[значения] = set()
|
|
108
|
+
self.данные[значения].add(ид_документа)
|
|
109
|
+
|
|
110
|
+
def удалить(self, ид_документа: int, документ: Dict):
|
|
111
|
+
значения = tuple(self.получить_значение(документ, к) for к in self.ключи)
|
|
112
|
+
if значения in self.данные:
|
|
113
|
+
self.данные[значения].discard(ид_документа)
|
|
114
|
+
if not self.данные[значения]:
|
|
115
|
+
del self.данные[значения]
|
|
116
|
+
|
|
117
|
+
def найти(self, значение: Any) -> set:
|
|
118
|
+
значения = tuple(значение) if isinstance(значение, (list, tuple)) else (значение,)
|
|
119
|
+
if значения in self.данные:
|
|
120
|
+
return self.данные[значения]
|
|
121
|
+
return set()
|
|
122
|
+
|
|
123
|
+
def найти_диапазон(self, от_: Any, до: Any) -> set:
|
|
124
|
+
результат = set()
|
|
125
|
+
for значения, ид in self.данные.items():
|
|
126
|
+
val = значения[0]
|
|
127
|
+
if от_ <= val <= до:
|
|
128
|
+
результат.update(ид)
|
|
129
|
+
return результат
|
|
130
|
+
|
|
131
|
+
def получить_значение(self, документ: Dict, ключ: str) -> Any:
|
|
132
|
+
ключи = ключ.split(".")
|
|
133
|
+
значение = документ
|
|
134
|
+
for к in ключи:
|
|
135
|
+
if isinstance(значение, dict):
|
|
136
|
+
значение = значение.get(к)
|
|
137
|
+
else:
|
|
138
|
+
return None
|
|
139
|
+
return значение
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Коробка:
|
|
143
|
+
def __init__(self, имя: str, база):
|
|
144
|
+
self.имя = имя
|
|
145
|
+
self.база = база
|
|
146
|
+
self.документы = []
|
|
147
|
+
self.счетчик = 0
|
|
148
|
+
self.индексы: Dict[str, Индекс] = {}
|
|
149
|
+
|
|
150
|
+
def положить(self, документ: Dict) -> int:
|
|
151
|
+
if not isinstance(документ, dict):
|
|
152
|
+
raise ОшибкаКоробки("Документ должен быть словарём")
|
|
153
|
+
ид = self.счетчик
|
|
154
|
+
документ["_id"] = ид
|
|
155
|
+
документ["_создан"] = datetime.now().isoformat()
|
|
156
|
+
self.документы.append(документ)
|
|
157
|
+
for индекс in self.индексы.values():
|
|
158
|
+
индекс.добавить(ид, документ)
|
|
159
|
+
self.счетчик += 1
|
|
160
|
+
return ид
|
|
161
|
+
|
|
162
|
+
def положить_много(self, документы: List[Dict]) -> List[int]:
|
|
163
|
+
ид_список = []
|
|
164
|
+
for документ in документы:
|
|
165
|
+
ид_список.append(self.положить(документ))
|
|
166
|
+
return ид_список
|
|
167
|
+
|
|
168
|
+
def найти(self, где: Dict = None, содержит: Dict = None, сортировка: str = None, лимит: int = None) -> List[Dict]:
|
|
169
|
+
условие = Условие()
|
|
170
|
+
if где:
|
|
171
|
+
условие.где(**где)
|
|
172
|
+
if содержит:
|
|
173
|
+
условие.содержит(**содержит)
|
|
174
|
+
if сортировка:
|
|
175
|
+
убыв = "убыв" in сортировка or "desc" in сортировка.lower()
|
|
176
|
+
ключ = сортировка.replace(" убыв", "").replace(" desc", "").replace(" возр", "").replace(" asc", "")
|
|
177
|
+
условие.сортировать_по(ключ, убыв)
|
|
178
|
+
if лимит:
|
|
179
|
+
условие.лимит(лимит)
|
|
180
|
+
return self.выполнить(условие)
|
|
181
|
+
|
|
182
|
+
def найти_один(self, где: Dict = None, **kwargs) -> Optional[Dict]:
|
|
183
|
+
if где is None and kwargs:
|
|
184
|
+
где = kwargs
|
|
185
|
+
результат = self.найти(где=где, лимит=1)
|
|
186
|
+
return результат[0] if результат else None
|
|
187
|
+
|
|
188
|
+
def найти_все(self) -> List[Dict]:
|
|
189
|
+
return self.документы.copy()
|
|
190
|
+
|
|
191
|
+
def найти_по_id(self, ид: int) -> Optional[Dict]:
|
|
192
|
+
for документ in self.документы:
|
|
193
|
+
if документ.get("_id") == ид:
|
|
194
|
+
return документ
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def считать(self, где: Dict = None) -> int:
|
|
198
|
+
если не где:
|
|
199
|
+
return len(self.документы)
|
|
200
|
+
return len(self.найти(где=где))
|
|
201
|
+
|
|
202
|
+
def выполнить(self, условие: Условие) -> List[Dict]:
|
|
203
|
+
результат = []
|
|
204
|
+
|
|
205
|
+
if условие.условия_и:
|
|
206
|
+
ид_кандидаты = None
|
|
207
|
+
for ключ, значение in условие.условия_и:
|
|
208
|
+
if ключ in self.индексы:
|
|
209
|
+
ид = self.индексы[ключ].найти(значение)
|
|
210
|
+
if ид_кандидаты is None:
|
|
211
|
+
ид_кандидаты = ид
|
|
212
|
+
else:
|
|
213
|
+
ид_кандидаты &= ид
|
|
214
|
+
if ид_кандидаты is not None:
|
|
215
|
+
результат = [d for d in self.документы if d["_id"] in ид_кандидаты]
|
|
216
|
+
else:
|
|
217
|
+
результат = self.документы.copy()
|
|
218
|
+
|
|
219
|
+
for ключ, значение in условие.условия_и:
|
|
220
|
+
if ид_кандидаты is None:
|
|
221
|
+
результат = [d for d in результат if self._проверить_условие(d, ключ, значение)]
|
|
222
|
+
else:
|
|
223
|
+
результат = self.документы.copy()
|
|
224
|
+
|
|
225
|
+
for ключ, значение in условие.условия_или:
|
|
226
|
+
результат_или = [d for d in self.документы if self._проверить_условие(d, ключ, значение)]
|
|
227
|
+
существующие_ид = {d["_id"] for d in результат}
|
|
228
|
+
for d in результат_или:
|
|
229
|
+
if d["_id"] not in существующие_ид:
|
|
230
|
+
результат.append(d)
|
|
231
|
+
существующие_ид.add(d["_id"])
|
|
232
|
+
|
|
233
|
+
for ключ, значение in условие.условия_не:
|
|
234
|
+
результат = [d for d in результат if not self._проверить_условие(d, ключ, значение)]
|
|
235
|
+
|
|
236
|
+
for ключ, значение in условие.условия_содержит:
|
|
237
|
+
результат = [d for d in результат if self._проверить_содержит(d, ключ, значение)]
|
|
238
|
+
|
|
239
|
+
if условие.сортировка:
|
|
240
|
+
результат.sort(key=lambda d: self._получить_ключ(d, условие.сортировка) or 0, reverse=условие.обратная_сортировка)
|
|
241
|
+
|
|
242
|
+
if условие.пропустить_значение:
|
|
243
|
+
результат = результат[условие.пропустить_значение:]
|
|
244
|
+
|
|
245
|
+
if условие.лимит_значение:
|
|
246
|
+
результат = результат[:условие.лимит_значение]
|
|
247
|
+
|
|
248
|
+
return результат
|
|
249
|
+
|
|
250
|
+
def обновить(self, где: Dict, данные: Dict) -> int:
|
|
251
|
+
найденные = self.найти(где=где)
|
|
252
|
+
for документ in найденные:
|
|
253
|
+
старый = документ.copy()
|
|
254
|
+
for ключ, значение in данные.items():
|
|
255
|
+
ключи = ключ.split(".")
|
|
256
|
+
цель = документ
|
|
257
|
+
for к in ключи[:-1]:
|
|
258
|
+
if к not in цель:
|
|
259
|
+
цель[k] = {}
|
|
260
|
+
цель = цель[k]
|
|
261
|
+
цель[ключи[-1]] = значение
|
|
262
|
+
for имя, индекс in self.индексы.items():
|
|
263
|
+
индекс.удалить(документ["_id"], старый)
|
|
264
|
+
индекс.добавить(документ["_id"], документ)
|
|
265
|
+
return len(найденные)
|
|
266
|
+
|
|
267
|
+
def обновить_по_id(self, ид: int, данные: Dict) -> bool:
|
|
268
|
+
документ = self.найти_по_id(ид)
|
|
269
|
+
if not документ:
|
|
270
|
+
return False
|
|
271
|
+
self.обновить(где={"_id": ид}, данные=данные)
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
def удалить(self, где: Dict = None) -> int:
|
|
275
|
+
if где is None:
|
|
276
|
+
return self.удалить_все()
|
|
277
|
+
найденные = self.найти(где=где)
|
|
278
|
+
for документ in найденные:
|
|
279
|
+
for имя, индекс in self.индексы.items():
|
|
280
|
+
индекс.удалить(документ["_id"], документ)
|
|
281
|
+
self.документы.remove(документ)
|
|
282
|
+
return len(найденные)
|
|
283
|
+
|
|
284
|
+
def удалить_по_id(self, ид: int) -> bool:
|
|
285
|
+
документ = self.найти_по_id(ид)
|
|
286
|
+
if not документ:
|
|
287
|
+
return False
|
|
288
|
+
self.удалить(где={"_id": ид})
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
def удалить_все(self) -> int:
|
|
292
|
+
count = len(self.документы)
|
|
293
|
+
self.документы.clear()
|
|
294
|
+
for индекс in self.индексы.values():
|
|
295
|
+
индекс.данные.clear()
|
|
296
|
+
self.счетчик = 0
|
|
297
|
+
return count
|
|
298
|
+
|
|
299
|
+
def есть(self, где: Dict) -> bool:
|
|
300
|
+
return len(self.найти(где=где, лимит=1)) > 0
|
|
301
|
+
|
|
302
|
+
def создать_индекс(self, ключ: Union[str, List[str]]):
|
|
303
|
+
имя = "_".join(ключ) if isinstance(ключ, list) else ключ
|
|
304
|
+
if имя in self.индексы:
|
|
305
|
+
return self.индексы[имя]
|
|
306
|
+
индекс = Индекс(имя, ключ)
|
|
307
|
+
self.индексы[имя] = индекс
|
|
308
|
+
for документ in self.документы:
|
|
309
|
+
индекс.добавить(документ["_id"], документ)
|
|
310
|
+
return индекс
|
|
311
|
+
|
|
312
|
+
def удалить_индекс(self, ключ: str):
|
|
313
|
+
if ключ in self.индексы:
|
|
314
|
+
del self.индексы[ключ]
|
|
315
|
+
|
|
316
|
+
def уникальные(self, ключ: str) -> List[Any]:
|
|
317
|
+
значения = set()
|
|
318
|
+
for документ in self.документы:
|
|
319
|
+
val = self._получить_ключ(документ, ключ)
|
|
320
|
+
if val is not None:
|
|
321
|
+
if isinstance(val, list):
|
|
322
|
+
for v in val:
|
|
323
|
+
значения.add(v)
|
|
324
|
+
else:
|
|
325
|
+
значения.add(val)
|
|
326
|
+
return sorted(значения, key=str)
|
|
327
|
+
|
|
328
|
+
def ключи(self) -> List[str]:
|
|
329
|
+
все_ключи = set()
|
|
330
|
+
for документ in self.документы:
|
|
331
|
+
все_ключи.update(документ.keys())
|
|
332
|
+
return sorted(все_ключи)
|
|
333
|
+
|
|
334
|
+
def размер(self) -> int:
|
|
335
|
+
return len(self.документы)
|
|
336
|
+
|
|
337
|
+
def _проверить_условие(self, документ: Dict, ключ: str, значение: Any) -> bool:
|
|
338
|
+
val = self._получить_ключ(документ, ключ)
|
|
339
|
+
if isinstance(значение, str) and val is not None:
|
|
340
|
+
значение_str = str(значение)
|
|
341
|
+
if значение_str.startswith("> "):
|
|
342
|
+
try:
|
|
343
|
+
return float(val) > float(значение_str[2:])
|
|
344
|
+
except (ValueError, TypeError):
|
|
345
|
+
return str(val) > значение_str[2:]
|
|
346
|
+
elif значение_str.startswith("< "):
|
|
347
|
+
try:
|
|
348
|
+
return float(val) < float(значение_str[2:])
|
|
349
|
+
except (ValueError, TypeError):
|
|
350
|
+
return str(val) < значение_str[2:]
|
|
351
|
+
elif значение_str.startswith(">= "):
|
|
352
|
+
return float(val) >= float(значение_str[3:])
|
|
353
|
+
elif значение_str.startswith("<= "):
|
|
354
|
+
return float(val) <= float(значение_str[3:])
|
|
355
|
+
elif значение_str.startswith("!= "):
|
|
356
|
+
return val != значение_str[3:]
|
|
357
|
+
elif "между" in значение_str and " и " in значение_str:
|
|
358
|
+
parts = значение_str.replace("между ", "").split(" и ")
|
|
359
|
+
try:
|
|
360
|
+
return float(parts[0]) <= float(val) <= float(parts[1])
|
|
361
|
+
except (ValueError, TypeError):
|
|
362
|
+
return str(parts[0]) <= str(val) <= str(parts[1])
|
|
363
|
+
elif значение_str.startswith("содержит "):
|
|
364
|
+
search_val = значение_str[9:]
|
|
365
|
+
if isinstance(val, str):
|
|
366
|
+
return search_val.lower() in val.lower()
|
|
367
|
+
if isinstance(val, list):
|
|
368
|
+
return search_val in val
|
|
369
|
+
return str(search_val) in str(val)
|
|
370
|
+
return val == значение
|
|
371
|
+
|
|
372
|
+
def _проверить_содержит(self, документ: Dict, ключ: str, значение: Any) -> bool:
|
|
373
|
+
val = self._получить_ключ(документ, ключ)
|
|
374
|
+
if isinstance(val, list):
|
|
375
|
+
return значение in val
|
|
376
|
+
if isinstance(val, str):
|
|
377
|
+
return str(значение) in val
|
|
378
|
+
if isinstance(val, dict):
|
|
379
|
+
return значение in val.values()
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
def _получить_ключ(self, документ: Dict, ключ: str) -> Any:
|
|
383
|
+
ключи = ключ.split(".")
|
|
384
|
+
значение = документ
|
|
385
|
+
for к in ключи:
|
|
386
|
+
if isinstance(значение, dict):
|
|
387
|
+
значение = значение.get(к)
|
|
388
|
+
else:
|
|
389
|
+
return None
|
|
390
|
+
return значение
|
|
391
|
+
|
|
392
|
+
def экспорт(self) -> List[Dict]:
|
|
393
|
+
return self.документы.copy()
|
|
394
|
+
|
|
395
|
+
def импорт(self, данные: List[Dict]):
|
|
396
|
+
for документ in данные:
|
|
397
|
+
if "_id" in документ:
|
|
398
|
+
del документ["_id"]
|
|
399
|
+
if "_создан" in документ:
|
|
400
|
+
del документ["_создан"]
|
|
401
|
+
self.положить(документ)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class База:
|
|
405
|
+
def __init__(self, имя: str, путь: str = None, ключ: str = None):
|
|
406
|
+
self.имя = имя
|
|
407
|
+
self.путь = путь or f"{имя}.json"
|
|
408
|
+
if ключ:
|
|
409
|
+
self.путь = self.путь.replace(".json", ".enc")
|
|
410
|
+
self.ключ = ключ
|
|
411
|
+
self.коробки: Dict[str, Коробка] = {}
|
|
412
|
+
self.загрузить()
|
|
413
|
+
|
|
414
|
+
def коробка(self, имя: str) -> Коробка:
|
|
415
|
+
if имя not in self.коробки:
|
|
416
|
+
self.коробки[имя] = Коробка(имя, self)
|
|
417
|
+
return self.коробки[имя]
|
|
418
|
+
|
|
419
|
+
def сохранить(self):
|
|
420
|
+
данные = {}
|
|
421
|
+
for имя, коробка in self.коробки.items():
|
|
422
|
+
данные[имя] = коробка.экспорт()
|
|
423
|
+
|
|
424
|
+
if self.ключ:
|
|
425
|
+
json_данные = json.dumps(данные, ensure_ascii=False, indent=2)
|
|
426
|
+
зашифровано = Шифр.зашифровать(json_данные, self.ключ)
|
|
427
|
+
with open(self.путь, 'wb') as ф:
|
|
428
|
+
ф.write(зашифровано)
|
|
429
|
+
else:
|
|
430
|
+
with open(self.путь, 'w', encoding='utf-8') as ф:
|
|
431
|
+
json.dump(данные, ф, ensure_ascii=False, indent=2)
|
|
432
|
+
|
|
433
|
+
def загрузить(self):
|
|
434
|
+
if not os.path.exists(self.путь):
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
if self.ключ:
|
|
439
|
+
with open(self.путь, 'rb') as ф:
|
|
440
|
+
зашифровано = ф.read()
|
|
441
|
+
json_данные = Шифр.расшифровать(зашифровано, self.ключ)
|
|
442
|
+
данные = json.loads(json_данные)
|
|
443
|
+
else:
|
|
444
|
+
with open(self.путь, 'r', encoding='utf-8') as ф:
|
|
445
|
+
данные = json.load(ф)
|
|
446
|
+
|
|
447
|
+
for имя, документы in данные.items():
|
|
448
|
+
коробка = self.коробка(имя)
|
|
449
|
+
коробка.удалить_все()
|
|
450
|
+
коробка.импорт(документы)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
raise ОшибкаБазы(f"Не удалось загрузить базу: {e}")
|
|
453
|
+
|
|
454
|
+
def сохранить_как(self, путь: str):
|
|
455
|
+
старый_путь = self.путь
|
|
456
|
+
self.путь = путь
|
|
457
|
+
self.сохранить()
|
|
458
|
+
self.путь = старый_путь
|
|
459
|
+
|
|
460
|
+
def бэкап(self):
|
|
461
|
+
временная_метка = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
462
|
+
имя_бэкапа = self.путь.replace(".json", f"_{временная_метка}.json").replace(".enc", f"_{временная_метка}.enc")
|
|
463
|
+
shutil.copy2(self.путь, имя_бэкапа)
|
|
464
|
+
return имя_бэкапа
|
|
465
|
+
|
|
466
|
+
def очистить(self):
|
|
467
|
+
for коробка in self.коробки.values():
|
|
468
|
+
коробка.удалить_все()
|
|
469
|
+
self.коробки.clear()
|
|
470
|
+
if os.path.exists(self.путь):
|
|
471
|
+
os.remove(self.путь)
|
|
472
|
+
|
|
473
|
+
def список_коробок(self) -> List[str]:
|
|
474
|
+
return sorted(self.коробки.keys())
|
|
475
|
+
|
|
476
|
+
def размер(self) -> int:
|
|
477
|
+
if os.path.exists(self.путь):
|
|
478
|
+
return os.path.getsize(self.путь)
|
|
479
|
+
return 0
|
|
480
|
+
|
|
481
|
+
def инфо(self) -> Dict:
|
|
482
|
+
return {
|
|
483
|
+
"имя": self.имя,
|
|
484
|
+
"путь": self.путь,
|
|
485
|
+
"зашифрована": bool(self.ключ),
|
|
486
|
+
"коробок": len(self.коробки),
|
|
487
|
+
"размер_байт": self.размер(),
|
|
488
|
+
"документов": sum(к.размер() for к in self.коробки.values())
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def __enter__(self):
|
|
492
|
+
return self
|
|
493
|
+
|
|
494
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
495
|
+
self.сохранить()
|
|
496
|
+
|
|
497
|
+
def __repr__(self):
|
|
498
|
+
return f"База('{self.имя}', коробок: {len(self.коробки)}, размер: {self.размер()} байт)"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class Версия:
|
|
502
|
+
НОМЕР = "1.0.0"
|
|
503
|
+
НАЗВАНИЕ = "VRBase"
|
|
504
|
+
АВТОР = "Pantapan1 & RuGram Team"
|
|
505
|
+
|
|
506
|
+
@staticmethod
|
|
507
|
+
def инфо():
|
|
508
|
+
return f"{Версия.НАЗВАНИЕ} v{Версия.НОМЕР} by {Версия.АВТОР}"
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
print(Версия.инфо())
|