danielutils 1.1.28__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.
- danielutils/__init__.py +50 -0
- danielutils/abstractions/__init__.py +3 -0
- danielutils/abstractions/db/__init__.py +10 -0
- danielutils/abstractions/db/database.py +206 -0
- danielutils/abstractions/db/database_definitions.py +219 -0
- danielutils/abstractions/db/database_exceptions.py +34 -0
- danielutils/abstractions/db/database_factory.py +70 -0
- danielutils/abstractions/db/database_initializer.py +261 -0
- danielutils/abstractions/db/database_models.py +213 -0
- danielutils/abstractions/db/dependencies.py +6 -0
- danielutils/abstractions/db/implementations/__init__.py +4 -0
- danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
- danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
- danielutils/abstractions/db/implementations/redis_database.py +403 -0
- danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
- danielutils/abstractions/multiprogramming/__init__.py +3 -0
- danielutils/abstractions/multiprogramming/multi_id.py +26 -0
- danielutils/abstractions/multiprogramming/worker.py +104 -0
- danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
- danielutils/abstractions/repl.py +125 -0
- danielutils/aliases.py +174 -0
- danielutils/async_/__init__.py +7 -0
- danielutils/async_/async_cmd.py +28 -0
- danielutils/async_/async_command.py +879 -0
- danielutils/async_/async_layered_command.py +125 -0
- danielutils/async_/async_retry_executor.py +86 -0
- danielutils/async_/async_worker_pool.py +110 -0
- danielutils/async_/time_strategy.py +73 -0
- danielutils/async_/utils.py +57 -0
- danielutils/better_builtins/__init__.py +3 -0
- danielutils/better_builtins/counter.py +48 -0
- danielutils/better_builtins/frange.py +266 -0
- danielutils/better_builtins/typed_builtins/__init__.py +4 -0
- danielutils/better_builtins/typed_builtins/factory.py +126 -0
- danielutils/better_builtins/typed_builtins/tdict.py +36 -0
- danielutils/better_builtins/typed_builtins/tlist.py +223 -0
- danielutils/better_builtins/typed_builtins/tset.py +67 -0
- danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
- danielutils/colors.py +203 -0
- danielutils/context_managers/__init__.py +5 -0
- danielutils/context_managers/attr_context.py +40 -0
- danielutils/context_managers/multi_context.py +34 -0
- danielutils/context_managers/optional_context.py +33 -0
- danielutils/context_managers/state_context.py +25 -0
- danielutils/context_managers/temporary_file.py +108 -0
- danielutils/convenience.py +12 -0
- danielutils/conversions/__init__.py +2 -0
- danielutils/conversions/main_conversions.py +131 -0
- danielutils/conversions/specialized_conversions/__init__.py +2 -0
- danielutils/conversions/specialized_conversions/to_hex.py +25 -0
- danielutils/conversions/specialized_conversions/to_int.py +19 -0
- danielutils/custom_types.py +18 -0
- danielutils/data_structures/__init__.py +8 -0
- danielutils/data_structures/algorithms.py +52 -0
- danielutils/data_structures/comparer.py +47 -0
- danielutils/data_structures/default_dict.py +23 -0
- danielutils/data_structures/functions.py +35 -0
- danielutils/data_structures/graph/__init__.py +4 -0
- danielutils/data_structures/graph/binary_node.py +71 -0
- danielutils/data_structures/graph/graph.py +208 -0
- danielutils/data_structures/graph/multinode.py +101 -0
- danielutils/data_structures/graph/node.py +60 -0
- danielutils/data_structures/heap/__init__.py +3 -0
- danielutils/data_structures/heap/heap.py +119 -0
- danielutils/data_structures/heap/max_heap.py +18 -0
- danielutils/data_structures/heap/min_heap.py +18 -0
- danielutils/data_structures/queue/__init__.py +3 -0
- danielutils/data_structures/queue/atomic_queue.py +15 -0
- danielutils/data_structures/queue/priority_queue.py +110 -0
- danielutils/data_structures/queue/queue.py +92 -0
- danielutils/data_structures/stack.py +102 -0
- danielutils/data_structures/trees/__init__.py +2 -0
- danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
- danielutils/data_structures/trees/binary_tree.py +63 -0
- danielutils/date.py +43 -0
- danielutils/date_time.py +23 -0
- danielutils/decorators/__init__.py +18 -0
- danielutils/decorators/atomic.py +45 -0
- danielutils/decorators/attach.py +61 -0
- danielutils/decorators/chain_decorators.py +39 -0
- danielutils/decorators/decorate_conditionally.py +32 -0
- danielutils/decorators/delay_call.py +47 -0
- danielutils/decorators/deprecate.py +46 -0
- danielutils/decorators/final.py +39 -0
- danielutils/decorators/limit_recursion.py +63 -0
- danielutils/decorators/memo.py +68 -0
- danielutils/decorators/normalize_decorator.py +55 -0
- danielutils/decorators/overload.py +204 -0
- danielutils/decorators/partially_implemented.py +32 -0
- danielutils/decorators/processify.py +74 -0
- danielutils/decorators/property.py +42 -0
- danielutils/decorators/singleton.py +59 -0
- danielutils/decorators/threadify.py +40 -0
- danielutils/decorators/timeout.py +82 -0
- danielutils/decorators/total_ordering.py +82 -0
- danielutils/decorators/validate.py +227 -0
- danielutils/exceptions.py +90 -0
- danielutils/functions/__init__.py +13 -0
- danielutils/functions/areoneof.py +47 -0
- danielutils/functions/check_foreach.py +39 -0
- danielutils/functions/factorial.py +24 -0
- danielutils/functions/flatten.py +33 -0
- danielutils/functions/foreach.py +20 -0
- danielutils/functions/isoftype.py +475 -0
- danielutils/functions/isoneof.py +71 -0
- danielutils/functions/multiloop.py +51 -0
- danielutils/functions/parallel_for.py +47 -0
- danielutils/functions/partition.py +45 -0
- danielutils/functions/powerset.py +31 -0
- danielutils/functions/subseteq.py +25 -0
- danielutils/functions/types_subseteq.py +51 -0
- danielutils/generators/__init__.py +3 -0
- danielutils/generators/conditional_generator.py +59 -0
- danielutils/generators/generator_from_stream.py +31 -0
- danielutils/generators/join_generators.py +109 -0
- danielutils/internet.py +92 -0
- danielutils/io_.py +451 -0
- danielutils/java/__init__.py +18 -0
- danielutils/java/interfaces/__init__.py +1 -0
- danielutils/java/interfaces/comparable.py +31 -0
- danielutils/java/java_interface.py +59 -0
- danielutils/logging_/__init__.py +2 -0
- danielutils/logging_/_impl/__init__.py +4 -0
- danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
- danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
- danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
- danielutils/logging_/_impl/log_level.py +13 -0
- danielutils/logging_/_impl/logger.py +109 -0
- danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
- danielutils/logging_/utils.py +199 -0
- danielutils/lombok/__init__.py +1 -0
- danielutils/lombok/builder.py +68 -0
- danielutils/math_/__init__.py +2 -0
- danielutils/math_/constants.py +103 -0
- danielutils/math_/functions.py +34 -0
- danielutils/math_/math_print.py +52 -0
- danielutils/math_/math_symbols.py +125 -0
- danielutils/metaclasses/__init__.py +5 -0
- danielutils/metaclasses/atomic_class_meta.py +36 -0
- danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
- danielutils/metaclasses/instance_cache_meta.py +66 -0
- danielutils/metaclasses/interface.py +328 -0
- danielutils/metaclasses/overload_meta.py +48 -0
- danielutils/mock_/__init__.py +1 -0
- danielutils/mock_/mock_module.py +27 -0
- danielutils/path.py +55 -0
- danielutils/print_.py +123 -0
- danielutils/progress_bar/__init__.py +3 -0
- danielutils/progress_bar/ascii_progress_bar.py +161 -0
- danielutils/progress_bar/progress_bar.py +75 -0
- danielutils/progress_bar/progress_bar_pool.py +72 -0
- danielutils/protocols/__init__.py +3 -0
- danielutils/protocols/dictable.py +38 -0
- danielutils/protocols/evaluable.py +26 -0
- danielutils/protocols/serializable.py +51 -0
- danielutils/py.typed +0 -0
- danielutils/random_.py +47 -0
- danielutils/reflection/__init__.py +6 -0
- danielutils/reflection/declarations.py +288 -0
- danielutils/reflection/file/__init__.py +1 -0
- danielutils/reflection/file/file_reflection.py +85 -0
- danielutils/reflection/function/__init__.py +1 -0
- danielutils/reflection/function/function_reflections.py +152 -0
- danielutils/reflection/info_classes/__init__.py +6 -0
- danielutils/reflection/info_classes/argument_info.py +278 -0
- danielutils/reflection/info_classes/class_info.py +204 -0
- danielutils/reflection/info_classes/decorator_info.py +54 -0
- danielutils/reflection/info_classes/file_info.py +1180 -0
- danielutils/reflection/info_classes/folder_info.py +592 -0
- danielutils/reflection/info_classes/function_info.py +551 -0
- danielutils/reflection/info_classes/import_info.py +207 -0
- danielutils/reflection/interpreter/__init__.py +9 -0
- danielutils/reflection/interpreter/callstack.py +57 -0
- danielutils/reflection/interpreter/get_traceback.py +27 -0
- danielutils/reflection/interpreter/interpreter.py +41 -0
- danielutils/reflection/interpreter/is_debugging.py +13 -0
- danielutils/reflection/interpreter/os_.py +33 -0
- danielutils/reflection/interpreter/packages.py +33 -0
- danielutils/reflection/interpreter/python_version.py +33 -0
- danielutils/reflection/interpreter/signals.py +18 -0
- danielutils/reflection/interpreter/tracer.py +175 -0
- danielutils/reflection/module/__init__.py +3 -0
- danielutils/reflection/module/lazy_module.py +104 -0
- danielutils/reflection/module/module_reflections.py +34 -0
- danielutils/reflection/module/package_reflection.py +161 -0
- danielutils/retry_executor/__init__.py +3 -0
- danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
- danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
- danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
- danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
- danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
- danielutils/retry_executor/backoff_strategy.py +28 -0
- danielutils/retry_executor/retry_executor.py +55 -0
- danielutils/snippets/__init__.py +1 -0
- danielutils/snippets/try_get.py +28 -0
- danielutils/system/__init__.py +3 -0
- danielutils/system/independent.py +197 -0
- danielutils/system/layered_command.py +130 -0
- danielutils/system/windows/__init__.py +2 -0
- danielutils/system/windows/utils/__init__.py +1 -0
- danielutils/system/windows/utils/filetime.py +19 -0
- danielutils/system/windows/win32_ctime.py +183 -0
- danielutils/system/windows/windows.py +78 -0
- danielutils/testing/__init__.py +1 -0
- danielutils/testing/unittest_/__init__.py +2 -0
- danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
- danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
- danielutils/text.py +169 -0
- danielutils/time.py +64 -0
- danielutils/tools/__init__.py +1 -0
- danielutils/tools/unittest_test_runner/__init__.py +9 -0
- danielutils/tools/unittest_test_runner/cli.py +70 -0
- danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
- danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
- danielutils/tools/unittest_test_runner/core/execution.py +244 -0
- danielutils/tools/unittest_test_runner/core/parser.py +163 -0
- danielutils/tools/unittest_test_runner/core/runner.py +561 -0
- danielutils/tools/unittest_test_runner/models.py +82 -0
- danielutils/university/__init__.py +7 -0
- danielutils/university/computability_and_complexity/__init__.py +2 -0
- danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
- danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
- danielutils/university/computability_and_complexity/languages/language.py +17 -0
- danielutils/university/computability_and_complexity/languages/sat.py +71 -0
- danielutils/university/computability_and_complexity/turing_machine.py +47 -0
- danielutils/university/databases/__init__.py +1 -0
- danielutils/university/databases/all.py +756 -0
- danielutils/university/image_proccesing/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/__init__.py +3 -0
- danielutils/university/image_proccesing/encoding/encoding.py +35 -0
- danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
- danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
- danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
- danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
- danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
- danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
- danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
- danielutils/university/image_proccesing/tansformations/hough.py +0 -0
- danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
- danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
- danielutils/university/linear_algebra/__init__.py +1 -0
- danielutils/university/linear_algebra/matrix.py +404 -0
- danielutils/university/machine_learning/__init__.py +0 -0
- danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
- danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
- danielutils/university/machine_learning/activation_functions/relu.py +11 -0
- danielutils/university/machine_learning/neuron.py +15 -0
- danielutils/university/oop/__init__.py +1 -0
- danielutils/university/oop/observer.py +31 -0
- danielutils/university/oop/strategy.py +9 -0
- danielutils/university/probability/__init__.py +7 -0
- danielutils/university/probability/conditional_variable/__init__.py +3 -0
- danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
- danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
- danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
- danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
- danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
- danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
- danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
- danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
- danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
- danielutils/university/probability/distributions.py +35 -0
- danielutils/university/probability/expressions/__init__.py +2 -0
- danielutils/university/probability/expressions/accumulation_expression.py +291 -0
- danielutils/university/probability/expressions/probability_expression.py +110 -0
- danielutils/university/probability/funcs/__init__.py +4 -0
- danielutils/university/probability/funcs/covariance.py +12 -0
- danielutils/university/probability/funcs/expected_value.py +41 -0
- danielutils/university/probability/funcs/probability_function.py +14 -0
- danielutils/university/probability/funcs/variance.py +17 -0
- danielutils/university/probability/operator.py +79 -0
- danielutils/university/probability/protocols.py +32 -0
- danielutils/university/probability/supp.py +101 -0
- danielutils/university/probability/transformation.py +44 -0
- danielutils/versioned_imports.py +31 -0
- danielutils-1.1.28.dist-info/METADATA +390 -0
- danielutils-1.1.28.dist-info/RECORD +287 -0
- danielutils-1.1.28.dist-info/WHEEL +5 -0
- danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
- danielutils-1.1.28.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Any, List, Type, Union, Sequence
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from ....logging_.utils import get_logger
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from starlette import status
|
|
8
|
+
except ImportError:
|
|
9
|
+
from ....mock_ import MockImportObject
|
|
10
|
+
|
|
11
|
+
status = MockImportObject("`starlette` is not installed") # type:ignore
|
|
12
|
+
|
|
13
|
+
from ..database import Database, TableSchema
|
|
14
|
+
from ..database_definitions import Operator, WhereClause, Condition, SelectQuery, UpdateQuery, DeleteQuery, ColumnType, \
|
|
15
|
+
TableColumn
|
|
16
|
+
from ..database_exceptions import DBValidationError, DBQueryError, DBConnectionError, DBException
|
|
17
|
+
|
|
18
|
+
class InMemoryDatabase(Database):
|
|
19
|
+
"""In-memory database implementation using dictionaries"""
|
|
20
|
+
|
|
21
|
+
def is_connected(self) -> bool:
|
|
22
|
+
return self._connected
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _default_class_exception_conversion(cls, e: Exception) -> Exception:
|
|
26
|
+
"""
|
|
27
|
+
Convert in-memory database specific exceptions to standard database exceptions.
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(e, ValueError):
|
|
30
|
+
return DBValidationError(f"Validation error: {str(e)}")
|
|
31
|
+
if isinstance(e, RuntimeError):
|
|
32
|
+
return DBQueryError(f"Query error: {str(e)}")
|
|
33
|
+
return DBException(f"Database error: {str(e)}")
|
|
34
|
+
|
|
35
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
36
|
+
try:
|
|
37
|
+
import starlette
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
raise ImportError(f"You must install required dependencies for {self.__class__.__name__}") from e
|
|
40
|
+
# table_name -> {id -> row}
|
|
41
|
+
self.tables: Dict[str, Dict[str, Any]] = {}
|
|
42
|
+
self.schemas: Dict[str, TableSchema] = {} # table_name -> TableSchema
|
|
43
|
+
# table_name -> {column_name -> last_used_id}
|
|
44
|
+
self.auto_increment_counters: Dict[str, Dict[str, int]] = {}
|
|
45
|
+
self._connected = False
|
|
46
|
+
self.logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
async def connect(self) -> None:
|
|
49
|
+
"""Connect to the database (no-op for in-memory)"""
|
|
50
|
+
self._connected = True
|
|
51
|
+
self.logger.info("Connected to in-memory database")
|
|
52
|
+
|
|
53
|
+
async def disconnect(self) -> None:
|
|
54
|
+
"""Disconnect from the database (no-op for in-memory)"""
|
|
55
|
+
self._connected = False
|
|
56
|
+
self.logger.info("Disconnected from in-memory database")
|
|
57
|
+
|
|
58
|
+
async def create_table(self, schema: TableSchema) -> None:
|
|
59
|
+
"""Create a new table with the given schema"""
|
|
60
|
+
if not self.is_connected():
|
|
61
|
+
raise RuntimeError("Not connected to database")
|
|
62
|
+
|
|
63
|
+
if schema.name in self.tables:
|
|
64
|
+
raise ValueError(f"Table '{schema.name}' already exists")
|
|
65
|
+
|
|
66
|
+
# Store schema
|
|
67
|
+
self.schemas[schema.name] = schema
|
|
68
|
+
|
|
69
|
+
# Create table with empty data
|
|
70
|
+
self.tables[schema.name] = {}
|
|
71
|
+
|
|
72
|
+
# Initialize auto-increment counters for this table
|
|
73
|
+
self.auto_increment_counters[schema.name] = {}
|
|
74
|
+
for column in schema.columns:
|
|
75
|
+
if column.type == ColumnType.AUTOINCREMENT:
|
|
76
|
+
self.auto_increment_counters[schema.name][column.name] = 0
|
|
77
|
+
|
|
78
|
+
self.logger.info("Created table '%s'", schema.name)
|
|
79
|
+
|
|
80
|
+
def _get_next_auto_increment_id(self, table: str, column: str) -> int:
|
|
81
|
+
"""Get the next available ID for an auto-increment column"""
|
|
82
|
+
if table not in self.auto_increment_counters:
|
|
83
|
+
raise ValueError(f"Table '{table}' does not exist")
|
|
84
|
+
if column not in self.auto_increment_counters[table]:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Column '{column}' is not an auto-increment column")
|
|
87
|
+
|
|
88
|
+
self.auto_increment_counters[table][column] += 1
|
|
89
|
+
return self.auto_increment_counters[table][column]
|
|
90
|
+
|
|
91
|
+
def _revert_auto_increment_counters(self, table: str, column: str) -> None:
|
|
92
|
+
"""Revert auto-increment counters"""
|
|
93
|
+
if table not in self.auto_increment_counters:
|
|
94
|
+
raise ValueError(f"Table '{table}' does not exist")
|
|
95
|
+
if column not in self.auto_increment_counters[table]:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Column '{column}' is not an auto-increment column")
|
|
98
|
+
self.auto_increment_counters[table][column] -= 1
|
|
99
|
+
|
|
100
|
+
async def get_schemas(self) -> Dict[str, TableSchema]:
|
|
101
|
+
"""Get all table schemas"""
|
|
102
|
+
if not self.is_connected():
|
|
103
|
+
raise RuntimeError("Not connected to database")
|
|
104
|
+
|
|
105
|
+
return {schema.name: schema for schema in self.schemas.values()}
|
|
106
|
+
|
|
107
|
+
async def insert(self, table: str, data: Dict[str, Any]) -> Any:
|
|
108
|
+
"""Insert a new row into the table"""
|
|
109
|
+
if not self.is_connected():
|
|
110
|
+
raise RuntimeError("Not connected to database")
|
|
111
|
+
|
|
112
|
+
if table not in self.tables:
|
|
113
|
+
raise ValueError(f"Table '{table}' does not exist")
|
|
114
|
+
|
|
115
|
+
schema = self.schemas[table]
|
|
116
|
+
row_data = data.copy()
|
|
117
|
+
|
|
118
|
+
autoincrements = []
|
|
119
|
+
# Handle auto-increment columns
|
|
120
|
+
for column in schema.columns:
|
|
121
|
+
if column.type == ColumnType.AUTOINCREMENT:
|
|
122
|
+
if column.name in data and data[column.name] is not None:
|
|
123
|
+
raise DBValidationError(
|
|
124
|
+
f"Cannot specify value for auto-increment column '{column.name}'")
|
|
125
|
+
autoincrements.append(column)
|
|
126
|
+
row_data[column.name] = self._get_next_auto_increment_id(
|
|
127
|
+
table, column.name)
|
|
128
|
+
# Generalize: handle any column with a callable default
|
|
129
|
+
columns_to_override_with_defualt = filter(
|
|
130
|
+
lambda column: callable(getattr(column, "default")) and getattr(row_data, column.name, None) is None,
|
|
131
|
+
schema.columns
|
|
132
|
+
)
|
|
133
|
+
for column in columns_to_override_with_defualt:
|
|
134
|
+
# IDK why, but even though this function is a supplier (no args) it
|
|
135
|
+
# raises an error if we dont pass some 'ctx' variable. So that's why we pass None
|
|
136
|
+
row_data[column.name] = column.default(None)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Validate data against schema
|
|
140
|
+
for column in schema.columns:
|
|
141
|
+
if not column.nullable and column.name not in row_data:
|
|
142
|
+
raise DBValidationError(
|
|
143
|
+
f"Required column '{column.name}' not provided")
|
|
144
|
+
if column.name in row_data and not self._validate_column_type(column, row_data[column.name]):
|
|
145
|
+
raise DBValidationError(
|
|
146
|
+
f"Invalid type for column '{column.name}'")
|
|
147
|
+
|
|
148
|
+
# Check unique constraints
|
|
149
|
+
if column.unique and column.name in row_data:
|
|
150
|
+
# Check if value already exists in any row
|
|
151
|
+
for existing_row in self.tables[table].values():
|
|
152
|
+
if self._values_equal(column.type, existing_row.get(column.name), row_data[column.name]):
|
|
153
|
+
raise DBValidationError(
|
|
154
|
+
f"Duplicate value for unique column '{column.name}'",
|
|
155
|
+
status_code=status.HTTP_409_CONFLICT)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
for column in autoincrements:
|
|
158
|
+
self._revert_auto_increment_counters(table, column.name)
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
# Add row
|
|
162
|
+
self.tables[table][row_data['id']] = row_data
|
|
163
|
+
self.logger.info(
|
|
164
|
+
"Inserted row into '%s' with ID '%s'", table, row_data['id'])
|
|
165
|
+
return row_data['id']
|
|
166
|
+
|
|
167
|
+
async def get(self, query: SelectQuery) -> List[Dict[str, Any]]:
|
|
168
|
+
"""Get rows from the table matching the query"""
|
|
169
|
+
if not self.is_connected():
|
|
170
|
+
raise DBConnectionError("Not connected to database")
|
|
171
|
+
|
|
172
|
+
if query.table not in self.tables:
|
|
173
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
174
|
+
|
|
175
|
+
# Get all rows
|
|
176
|
+
rows = list(self.tables[query.table].values())
|
|
177
|
+
|
|
178
|
+
# Apply where clause if present
|
|
179
|
+
if query.where:
|
|
180
|
+
column_names = set(
|
|
181
|
+
column.name for column in self.schemas[query.table].columns)
|
|
182
|
+
for condition in query.where.conditions:
|
|
183
|
+
if condition.column not in column_names:
|
|
184
|
+
raise DBQueryError(
|
|
185
|
+
f"Condition on invalid column '{condition.column}'")
|
|
186
|
+
rows = [row for row in rows if self._evaluate_where_clause(
|
|
187
|
+
row, query.where)]
|
|
188
|
+
|
|
189
|
+
# Apply order by if present
|
|
190
|
+
if query.order_by:
|
|
191
|
+
for order in reversed(query.order_by):
|
|
192
|
+
rows.sort(
|
|
193
|
+
key=lambda x: x[order.column],
|
|
194
|
+
reverse=order.direction == "DESC"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Apply limit and offset if present
|
|
198
|
+
if query.offset:
|
|
199
|
+
rows = rows[query.offset:]
|
|
200
|
+
if query.limit:
|
|
201
|
+
rows = rows[:query.limit]
|
|
202
|
+
|
|
203
|
+
return rows
|
|
204
|
+
|
|
205
|
+
async def update(self, query: UpdateQuery) -> int:
|
|
206
|
+
"""Update rows in the table matching the query"""
|
|
207
|
+
if not self.is_connected():
|
|
208
|
+
raise RuntimeError("Not connected to database")
|
|
209
|
+
|
|
210
|
+
if query.table not in self.tables:
|
|
211
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
212
|
+
|
|
213
|
+
# Get schema for validation
|
|
214
|
+
schema = self.schemas[query.table]
|
|
215
|
+
updated_count = 0
|
|
216
|
+
|
|
217
|
+
# Update matching rows
|
|
218
|
+
for row_id, row in self.tables[query.table].items():
|
|
219
|
+
if not query.where or self._evaluate_where_clause(row, query.where):
|
|
220
|
+
# Validate new values against schema
|
|
221
|
+
for column_name, value in query.data.items():
|
|
222
|
+
column = next(
|
|
223
|
+
(col for col in schema.columns if col.name == column_name), None)
|
|
224
|
+
if not column:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Column '{column_name}' does not exist")
|
|
227
|
+
if not self._validate_column_type(column, value):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Invalid type for column '{column_name}'")
|
|
230
|
+
|
|
231
|
+
# Update row
|
|
232
|
+
row.update(query.data)
|
|
233
|
+
updated_count += 1
|
|
234
|
+
|
|
235
|
+
self.logger.info("Updated '%s' rows in '%s'", updated_count, query.table)
|
|
236
|
+
return updated_count
|
|
237
|
+
|
|
238
|
+
async def delete(self, query: DeleteQuery) -> int:
|
|
239
|
+
"""Delete rows from the table matching the query"""
|
|
240
|
+
if not self.is_connected():
|
|
241
|
+
raise RuntimeError("Not connected to database")
|
|
242
|
+
|
|
243
|
+
if query.table not in self.tables:
|
|
244
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
245
|
+
|
|
246
|
+
# Find rows to delete
|
|
247
|
+
rows_to_delete = []
|
|
248
|
+
for row_id, row in self.tables[query.table].items():
|
|
249
|
+
if not query.where or self._evaluate_where_clause(row, query.where):
|
|
250
|
+
rows_to_delete.append(row_id)
|
|
251
|
+
|
|
252
|
+
# Delete rows
|
|
253
|
+
for row_id in rows_to_delete:
|
|
254
|
+
del self.tables[query.table][row_id]
|
|
255
|
+
|
|
256
|
+
self.logger.info(
|
|
257
|
+
"Deleted %d rows from '%s'", len(rows_to_delete), query.table)
|
|
258
|
+
return len(rows_to_delete)
|
|
259
|
+
|
|
260
|
+
def _validate_column_type(self, column: TableColumn, value: Any) -> bool:
|
|
261
|
+
"""Validate a value against a column's type"""
|
|
262
|
+
if value is None:
|
|
263
|
+
return column.nullable
|
|
264
|
+
|
|
265
|
+
type_map: Dict[str, Union[Type, Sequence[Type]]] = {
|
|
266
|
+
"INTEGER": int,
|
|
267
|
+
"BIGINT": int,
|
|
268
|
+
"FLOAT": float,
|
|
269
|
+
"DOUBLE": float,
|
|
270
|
+
"DECIMAL": float,
|
|
271
|
+
"BOOLEAN": bool,
|
|
272
|
+
"TEXT": str,
|
|
273
|
+
"VARCHAR": str,
|
|
274
|
+
"CHAR": str,
|
|
275
|
+
"DATE": (str, datetime),
|
|
276
|
+
"TIME": (str, datetime),
|
|
277
|
+
"DATETIME": (str, datetime),
|
|
278
|
+
"TIMESTAMP": (str, datetime),
|
|
279
|
+
"BLOB": bytes,
|
|
280
|
+
"JSON": (dict, list),
|
|
281
|
+
"UUID": str,
|
|
282
|
+
"AUTOINCREMENT": int
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
expected_type = type_map.get(column.type.value)
|
|
286
|
+
if not expected_type:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
if isinstance(expected_type, tuple):
|
|
290
|
+
return isinstance(value, expected_type)
|
|
291
|
+
return isinstance(value, expected_type)
|
|
292
|
+
|
|
293
|
+
def _is_case_sensitive_type(self, column_type: ColumnType) -> bool:
|
|
294
|
+
"""Check if a column type should use case-sensitive comparison"""
|
|
295
|
+
case_sensitive_types = {
|
|
296
|
+
ColumnType.BLOB,
|
|
297
|
+
ColumnType.JSON,
|
|
298
|
+
ColumnType.UUID
|
|
299
|
+
}
|
|
300
|
+
return column_type in case_sensitive_types
|
|
301
|
+
|
|
302
|
+
def _values_equal(self, col_type: ColumnType, val1: Any, val2: Any) -> bool:
|
|
303
|
+
"""Compare two values based on column type"""
|
|
304
|
+
if val1 is None or val2 is None:
|
|
305
|
+
return val1 is val2
|
|
306
|
+
|
|
307
|
+
if self._is_case_sensitive_type(col_type):
|
|
308
|
+
return val1 == val2
|
|
309
|
+
|
|
310
|
+
# Case-insensitive comparison for string types
|
|
311
|
+
if isinstance(val1, str) and isinstance(val2, str):
|
|
312
|
+
return val1.lower() == val2.lower()
|
|
313
|
+
|
|
314
|
+
return val1 == val2
|
|
315
|
+
|
|
316
|
+
def _evaluate_where_clause(self, row: Dict[str, Any], where_clause: WhereClause) -> bool:
|
|
317
|
+
"""Evaluate a where clause against a row"""
|
|
318
|
+
results = [self._evaluate_condition(
|
|
319
|
+
row, condition) for condition in where_clause.conditions]
|
|
320
|
+
if where_clause.operator == "AND":
|
|
321
|
+
return all(results)
|
|
322
|
+
return any(results)
|
|
323
|
+
|
|
324
|
+
def _evaluate_condition(self, row: Dict[str, Any], condition: Condition) -> bool:
|
|
325
|
+
"""Evaluate a single condition against a row"""
|
|
326
|
+
if condition.column not in row:
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
value = row[condition.column]
|
|
330
|
+
if condition.operator == Operator.EQ:
|
|
331
|
+
return value == condition.value
|
|
332
|
+
elif condition.operator == Operator.NEQ:
|
|
333
|
+
return value != condition.value
|
|
334
|
+
elif condition.operator == Operator.GT:
|
|
335
|
+
return value > condition.value
|
|
336
|
+
elif condition.operator == Operator.GTE:
|
|
337
|
+
return value >= condition.value
|
|
338
|
+
elif condition.operator == Operator.LT:
|
|
339
|
+
return value < condition.value
|
|
340
|
+
elif condition.operator == Operator.LTE:
|
|
341
|
+
return value <= condition.value
|
|
342
|
+
elif condition.operator == Operator.LIKE:
|
|
343
|
+
raise DBQueryError(
|
|
344
|
+
"LIKE operator is not supported in InMemoryDatabase. Use CONTAINS or CONTAINS_CS instead.")
|
|
345
|
+
elif condition.operator == Operator.ILIKE:
|
|
346
|
+
raise DBQueryError(
|
|
347
|
+
"ILIKE operator is not supported in InMemoryDatabase. Use CONTAINS or CONTAINS_CS instead.")
|
|
348
|
+
elif condition.operator == Operator.CONTAINS:
|
|
349
|
+
return str(condition.value).lower() in str(value).lower()
|
|
350
|
+
elif condition.operator == Operator.CONTAINS_CS:
|
|
351
|
+
return str(condition.value) in str(value)
|
|
352
|
+
elif condition.operator == Operator.IN:
|
|
353
|
+
return value in condition.values # type: ignore
|
|
354
|
+
elif condition.operator == Operator.NOT_IN:
|
|
355
|
+
return value not in condition.values # type: ignore
|
|
356
|
+
elif condition.operator == Operator.IS_NULL:
|
|
357
|
+
return value is None
|
|
358
|
+
elif condition.operator == Operator.IS_NOT_NULL:
|
|
359
|
+
return value is not None
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
__all__ = [
|
|
364
|
+
"InMemoryDatabase"
|
|
365
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Callable, Optional, Any, Dict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from .in_memory_database import InMemoryDatabase
|
|
6
|
+
from ..database_exceptions import DBException
|
|
7
|
+
from ..database_definitions import TableSchema, ColumnType, UpdateQuery, DeleteQuery
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DateTimeEncoder(json.JSONEncoder):
|
|
11
|
+
"""Custom JSON encoder for datetime objects"""
|
|
12
|
+
|
|
13
|
+
def default(self, obj: Any) -> Any:
|
|
14
|
+
if isinstance(obj, datetime):
|
|
15
|
+
return {
|
|
16
|
+
"__type__": "datetime",
|
|
17
|
+
"value": obj.isoformat()
|
|
18
|
+
}
|
|
19
|
+
return super().default(obj)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DateTimeDecoder(json.JSONDecoder):
|
|
23
|
+
"""Custom JSON decoder for datetime objects"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
26
|
+
super().__init__(object_hook=self._object_hook, *args, **kwargs)
|
|
27
|
+
|
|
28
|
+
def _object_hook(self, obj: dict) -> Any:
|
|
29
|
+
if "__type__" in obj and obj["__type__"] == "datetime":
|
|
30
|
+
return datetime.fromisoformat(obj["value"])
|
|
31
|
+
return obj
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PersistentInMemoryDatabase(InMemoryDatabase):
|
|
35
|
+
"""In-memory database with persistence to disk"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
data_dir: str,
|
|
40
|
+
*args,
|
|
41
|
+
auto_save: bool = False,
|
|
42
|
+
register_shutdown_handler: Optional[Callable[[Callable[[], None]], None]] = None,
|
|
43
|
+
**kwargs
|
|
44
|
+
) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Initialize persistent in-memory database
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data_dir (str): Directory to store database files
|
|
50
|
+
auto_save (bool): Whether to save state after every change (default: False)
|
|
51
|
+
register_shutdown_handler (Callable[[Callable[[], None]], None], optional):
|
|
52
|
+
Function to register shutdown handler. If provided, will be called with a function
|
|
53
|
+
that saves the database state.
|
|
54
|
+
"""
|
|
55
|
+
super().__init__()
|
|
56
|
+
self.data_dir = Path(data_dir).resolve()
|
|
57
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
self.auto_save = auto_save
|
|
59
|
+
|
|
60
|
+
# Register shutdown handler if provided
|
|
61
|
+
if register_shutdown_handler is not None:
|
|
62
|
+
register_shutdown_handler(self._save_state)
|
|
63
|
+
|
|
64
|
+
def _get_state_file(self) -> Path:
|
|
65
|
+
"""Get the path to the state file"""
|
|
66
|
+
return self.data_dir / "db_state.json"
|
|
67
|
+
|
|
68
|
+
def _save_state(self) -> None:
|
|
69
|
+
"""Save current database state to disk"""
|
|
70
|
+
try:
|
|
71
|
+
def filter_callable_defaults(schema_dict: Dict) -> Dict:
|
|
72
|
+
# Remove callables from column defaults
|
|
73
|
+
if 'columns' in schema_dict:
|
|
74
|
+
for col in schema_dict['columns']:
|
|
75
|
+
if isinstance(col.get('default'), (type(lambda: 0), type(filter_callable_defaults))):
|
|
76
|
+
col['default'] = None
|
|
77
|
+
elif callable(col.get('default')):
|
|
78
|
+
col['default'] = None
|
|
79
|
+
return schema_dict
|
|
80
|
+
|
|
81
|
+
state = {
|
|
82
|
+
'tables': self.tables,
|
|
83
|
+
'schemas': {
|
|
84
|
+
name: filter_callable_defaults(schema.model_dump())
|
|
85
|
+
for name, schema in self.schemas.items()
|
|
86
|
+
},
|
|
87
|
+
'auto_increment_counters': self.auto_increment_counters
|
|
88
|
+
}
|
|
89
|
+
with open(self._get_state_file(), 'w') as f:
|
|
90
|
+
json.dump(state, f, indent=2, cls=DateTimeEncoder)
|
|
91
|
+
self.logger.info("Database state saved successfully")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.logger.error("Error saving database state: %s", e)
|
|
94
|
+
raise DBException(f"Failed to save database state: {str(e)}")
|
|
95
|
+
|
|
96
|
+
def _load_state(self) -> None:
|
|
97
|
+
"""Load database state from disk if available"""
|
|
98
|
+
state_file = self._get_state_file()
|
|
99
|
+
if state_file.exists():
|
|
100
|
+
try:
|
|
101
|
+
with open(state_file, 'r') as f:
|
|
102
|
+
state = json.load(f, cls=DateTimeDecoder)
|
|
103
|
+
self.tables = state['tables']
|
|
104
|
+
# Convert schema dictionaries back to TableSchema objects
|
|
105
|
+
self.schemas = {
|
|
106
|
+
name: TableSchema.model_validate(schema_dict)
|
|
107
|
+
for name, schema_dict in state['schemas'].items()
|
|
108
|
+
}
|
|
109
|
+
# Load auto-increment counters
|
|
110
|
+
self.auto_increment_counters = state.get(
|
|
111
|
+
'auto_increment_counters', {})
|
|
112
|
+
|
|
113
|
+
# If auto_increment_counters is not in the state (old format),
|
|
114
|
+
# initialize it from the existing data
|
|
115
|
+
if not self.auto_increment_counters:
|
|
116
|
+
self.auto_increment_counters = {}
|
|
117
|
+
for table_name, schema in self.schemas.items():
|
|
118
|
+
self.auto_increment_counters[table_name] = {}
|
|
119
|
+
for column in schema.columns:
|
|
120
|
+
if column.type == ColumnType.AUTOINCREMENT:
|
|
121
|
+
# Find the highest value for this column
|
|
122
|
+
max_id = 0
|
|
123
|
+
for row in self.tables[table_name].values():
|
|
124
|
+
if column.name in row:
|
|
125
|
+
max_id = max(max_id, row[column.name])
|
|
126
|
+
self.auto_increment_counters[table_name][column.name] = max_id
|
|
127
|
+
|
|
128
|
+
self.logger.info("Database state loaded successfully")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.error("Error loading database state: %s", e)
|
|
131
|
+
raise DBException(f"Failed to load database state: {str(e)}")
|
|
132
|
+
|
|
133
|
+
def _maybe_save_state(self) -> None:
|
|
134
|
+
"""Save state if auto_save is enabled"""
|
|
135
|
+
if self.auto_save:
|
|
136
|
+
self._save_state()
|
|
137
|
+
|
|
138
|
+
async def connect(self) -> None:
|
|
139
|
+
"""Connect to the database and load state from disk"""
|
|
140
|
+
await super().connect()
|
|
141
|
+
self._load_state()
|
|
142
|
+
|
|
143
|
+
async def disconnect(self) -> None:
|
|
144
|
+
"""Close the database connection and save state"""
|
|
145
|
+
self._save_state()
|
|
146
|
+
await super().disconnect()
|
|
147
|
+
|
|
148
|
+
async def create_table(self, schema: TableSchema) -> None:
|
|
149
|
+
"""Create a new table with the given schema"""
|
|
150
|
+
await super().create_table(schema)
|
|
151
|
+
self._maybe_save_state()
|
|
152
|
+
|
|
153
|
+
async def insert(self, table: str, data: Dict[str, Any]) -> int:
|
|
154
|
+
"""Insert a new record into the table"""
|
|
155
|
+
result = await super().insert(table, data)
|
|
156
|
+
self._maybe_save_state()
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
async def update(self, query: UpdateQuery) -> int:
|
|
160
|
+
"""Update records in the table"""
|
|
161
|
+
result = await super().update(query)
|
|
162
|
+
self._maybe_save_state()
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
async def delete(self, query: DeleteQuery) -> int:
|
|
166
|
+
"""Delete records from the table"""
|
|
167
|
+
result = await super().delete(query)
|
|
168
|
+
self._maybe_save_state()
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = [
|
|
173
|
+
"PersistentInMemoryDatabase"
|
|
174
|
+
]
|