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.
@@ -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,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
+ 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(Версия.инфо())