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,403 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Dict, Any, Optional, Type, Union, Sequence
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from ....logging_.utils import get_logger
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import redis.asyncio as redis
|
|
9
|
+
except ImportError:
|
|
10
|
+
from ....mock_ import MockImportObject
|
|
11
|
+
|
|
12
|
+
redis = MockImportObject("`redis` is not installed") # type:ignore
|
|
13
|
+
|
|
14
|
+
from ..database import Database
|
|
15
|
+
from ..database_definitions import (
|
|
16
|
+
DeleteQuery, UpdateQuery, SelectQuery, TableSchema,
|
|
17
|
+
Operator, WhereClause, Condition, ColumnType, TableColumn
|
|
18
|
+
)
|
|
19
|
+
from ..database_exceptions import DBException, DBValidationError, DBQueryError, DBConnectionError
|
|
20
|
+
|
|
21
|
+
class RedisDatabase(Database):
|
|
22
|
+
"""Redis implementation of the Database abstract class"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _default_class_exception_conversion(cls, e: Exception) -> Exception:
|
|
26
|
+
"""
|
|
27
|
+
Convert Redis-specific exceptions to standard database exceptions.
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(e, redis.ConnectionError):
|
|
30
|
+
return DBConnectionError(f"Redis connection error: {str(e)}")
|
|
31
|
+
elif isinstance(e, redis.RedisError):
|
|
32
|
+
return DBException(f"Redis error: {str(e)}")
|
|
33
|
+
elif isinstance(e, ValueError):
|
|
34
|
+
return DBValidationError(f"Validation error: {str(e)}")
|
|
35
|
+
elif isinstance(e, RuntimeError):
|
|
36
|
+
return DBQueryError(f"Query error: {str(e)}")
|
|
37
|
+
return DBException(f"Database error: {str(e)}")
|
|
38
|
+
|
|
39
|
+
def __init__(self, host='localhost', port=6379, db=0, password=None, decode_responses=True) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialize Redis database connection
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
host (str): Redis host
|
|
45
|
+
port (int): Redis port
|
|
46
|
+
db (int): Redis database number
|
|
47
|
+
password (str, optional): Redis password
|
|
48
|
+
decode_responses (bool): Whether to decode responses to strings
|
|
49
|
+
"""
|
|
50
|
+
self.host = host
|
|
51
|
+
self.port = port
|
|
52
|
+
self.db = db
|
|
53
|
+
self.password = password
|
|
54
|
+
self.decode_responses = decode_responses
|
|
55
|
+
self._db: redis.Redis = None # type:ignore
|
|
56
|
+
self._connected = False
|
|
57
|
+
self.logger = get_logger(__name__)
|
|
58
|
+
|
|
59
|
+
# Redis key prefixes
|
|
60
|
+
self.SCHEMA_PREFIX = "schema:"
|
|
61
|
+
self.TABLE_PREFIX = "table:"
|
|
62
|
+
self.COUNTER_PREFIX = "counter:"
|
|
63
|
+
|
|
64
|
+
def is_connected(self) -> bool:
|
|
65
|
+
return self._connected
|
|
66
|
+
|
|
67
|
+
async def connect(self) -> None:
|
|
68
|
+
"""Establish connection to Redis database"""
|
|
69
|
+
try:
|
|
70
|
+
self._db = redis.Redis(
|
|
71
|
+
host=self.host,
|
|
72
|
+
port=self.port,
|
|
73
|
+
db=self.db,
|
|
74
|
+
password=self.password,
|
|
75
|
+
decode_responses=self.decode_responses
|
|
76
|
+
)
|
|
77
|
+
# Test connection
|
|
78
|
+
await self._db.ping()
|
|
79
|
+
self._connected = True
|
|
80
|
+
self.logger.info("Connected to Redis database at %s:%s", self.host, self.port)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
self.logger.error("Error connecting to Redis database: %s", e)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
async def disconnect(self) -> None:
|
|
86
|
+
"""Close the Redis database connection"""
|
|
87
|
+
if self._db:
|
|
88
|
+
await self._db.aclose()
|
|
89
|
+
self._connected = False
|
|
90
|
+
self.logger.info("Disconnected from Redis database")
|
|
91
|
+
|
|
92
|
+
def _assert_connection(self) -> None:
|
|
93
|
+
"""Check if connected to database"""
|
|
94
|
+
if not self._connected or not self._db:
|
|
95
|
+
raise DBConnectionError("Not connected to database")
|
|
96
|
+
|
|
97
|
+
async def get_schemas(self) -> Dict[str, TableSchema]:
|
|
98
|
+
"""Get all table schemas"""
|
|
99
|
+
self._assert_connection()
|
|
100
|
+
|
|
101
|
+
schemas = {}
|
|
102
|
+
# Get all schema keys
|
|
103
|
+
schema_keys = await self._db.keys(f"{self.SCHEMA_PREFIX}*")
|
|
104
|
+
|
|
105
|
+
for key in schema_keys:
|
|
106
|
+
table_name = key[len(self.SCHEMA_PREFIX):]
|
|
107
|
+
schema_json = await self._db.get(key)
|
|
108
|
+
if schema_json:
|
|
109
|
+
schema_dict = json.loads(schema_json)
|
|
110
|
+
schemas[table_name] = TableSchema.model_validate(schema_dict)
|
|
111
|
+
|
|
112
|
+
return schemas
|
|
113
|
+
|
|
114
|
+
async def create_table(self, schema: TableSchema) -> None:
|
|
115
|
+
"""Create a new table with the given schema"""
|
|
116
|
+
self._assert_connection()
|
|
117
|
+
|
|
118
|
+
schema_key = f"{self.SCHEMA_PREFIX}{schema.name}"
|
|
119
|
+
if await self._db.exists(schema_key):
|
|
120
|
+
raise ValueError(f"Table '{schema.name}' already exists")
|
|
121
|
+
|
|
122
|
+
# Store schema as JSON
|
|
123
|
+
schema_json = schema.to_json()
|
|
124
|
+
await self._db.set(schema_key, schema_json)
|
|
125
|
+
|
|
126
|
+
# Initialize auto-increment counters
|
|
127
|
+
for column in schema.columns:
|
|
128
|
+
if column.type == ColumnType.AUTOINCREMENT:
|
|
129
|
+
counter_key = f"{self.COUNTER_PREFIX}{schema.name}:{column.name}"
|
|
130
|
+
await self._db.set(counter_key, 0)
|
|
131
|
+
|
|
132
|
+
self.logger.info("Created table '%s'", schema.name)
|
|
133
|
+
|
|
134
|
+
async def _get_next_auto_increment_id(self, table: str, column: str) -> int:
|
|
135
|
+
"""Get the next available ID for an auto-increment column"""
|
|
136
|
+
counter_key = f"{self.COUNTER_PREFIX}{table}:{column}"
|
|
137
|
+
return await self._db.incr(counter_key)
|
|
138
|
+
|
|
139
|
+
def _validate_column_type(self, column: TableColumn, value: Any) -> bool:
|
|
140
|
+
"""Validate a value against a column's type"""
|
|
141
|
+
if value is None:
|
|
142
|
+
return column.nullable
|
|
143
|
+
|
|
144
|
+
type_map: Dict[str, Union[Type, Sequence[Type]]] = {
|
|
145
|
+
"INTEGER": int,
|
|
146
|
+
"BIGINT": int,
|
|
147
|
+
"FLOAT": float,
|
|
148
|
+
"DOUBLE": float,
|
|
149
|
+
"DECIMAL": float,
|
|
150
|
+
"BOOLEAN": bool,
|
|
151
|
+
"TEXT": str,
|
|
152
|
+
"VARCHAR": str,
|
|
153
|
+
"CHAR": str,
|
|
154
|
+
"DATE": (str, datetime),
|
|
155
|
+
"TIME": (str, datetime),
|
|
156
|
+
"DATETIME": (str, datetime),
|
|
157
|
+
"TIMESTAMP": (str, datetime),
|
|
158
|
+
"BLOB": bytes,
|
|
159
|
+
"JSON": (dict, list),
|
|
160
|
+
"UUID": str,
|
|
161
|
+
"AUTOINCREMENT": int
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
expected_type = type_map.get(column.type.value, None)
|
|
165
|
+
if not expected_type:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
if isinstance(expected_type, tuple):
|
|
169
|
+
return isinstance(value, expected_type)
|
|
170
|
+
return isinstance(value, expected_type) # type: ignore
|
|
171
|
+
|
|
172
|
+
async def insert(self, table: str, data: Dict[str, Any]) -> Any:
|
|
173
|
+
"""Insert a new record into the specified table"""
|
|
174
|
+
self._assert_connection()
|
|
175
|
+
|
|
176
|
+
# Get schema
|
|
177
|
+
schema_key = f"{self.SCHEMA_PREFIX}{table}"
|
|
178
|
+
schema_json = await self._db.get(schema_key)
|
|
179
|
+
if not schema_json:
|
|
180
|
+
raise ValueError(f"Table '{table}' does not exist")
|
|
181
|
+
|
|
182
|
+
schema = TableSchema.model_validate(json.loads(schema_json))
|
|
183
|
+
row_data = data.copy()
|
|
184
|
+
|
|
185
|
+
# Handle auto-increment columns
|
|
186
|
+
for column in schema.columns:
|
|
187
|
+
if column.type == ColumnType.AUTOINCREMENT:
|
|
188
|
+
if column.name in data and data[column.name] is not None:
|
|
189
|
+
raise DBValidationError(f"Cannot specify value for auto-increment column '{column.name}'")
|
|
190
|
+
row_data[column.name] = await self._get_next_auto_increment_id(table, column.name)
|
|
191
|
+
|
|
192
|
+
# Validate data against schema
|
|
193
|
+
for column in schema.columns:
|
|
194
|
+
if column.name in row_data:
|
|
195
|
+
if not self._validate_column_type(column, row_data[column.name]):
|
|
196
|
+
raise DBValidationError(f"Invalid type for column '{column.name}'")
|
|
197
|
+
elif not column.nullable and column.default is None:
|
|
198
|
+
raise DBValidationError(f"Required column '{column.name}' has no value")
|
|
199
|
+
|
|
200
|
+
# Generate unique ID for the row
|
|
201
|
+
row_id = str(await self._get_next_auto_increment_id(table, "_id"))
|
|
202
|
+
|
|
203
|
+
# Store row data as hash
|
|
204
|
+
table_key = f"{self.TABLE_PREFIX}{table}"
|
|
205
|
+
row_key = f"{row_id}"
|
|
206
|
+
|
|
207
|
+
# Convert data to strings for Redis hash
|
|
208
|
+
hash_data = {}
|
|
209
|
+
for key, value in row_data.items():
|
|
210
|
+
if isinstance(value, (dict, list)):
|
|
211
|
+
hash_data[key] = json.dumps(value)
|
|
212
|
+
else:
|
|
213
|
+
hash_data[key] = str(value)
|
|
214
|
+
|
|
215
|
+
await self._db.hset(table_key, row_key, json.dumps(hash_data)) # type: ignore
|
|
216
|
+
|
|
217
|
+
self.logger.info("Inserted row %s into table '%s'", row_id, table)
|
|
218
|
+
return row_id
|
|
219
|
+
|
|
220
|
+
def _evaluate_condition(self, row: Dict[str, Any], condition: Condition) -> bool:
|
|
221
|
+
"""Evaluate a single condition against a row"""
|
|
222
|
+
if condition.column not in row:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
value = row[condition.column]
|
|
226
|
+
if condition.operator == Operator.EQ:
|
|
227
|
+
return value == condition.value
|
|
228
|
+
elif condition.operator == Operator.NEQ:
|
|
229
|
+
return value != condition.value
|
|
230
|
+
elif condition.operator == Operator.GT:
|
|
231
|
+
return value > condition.value
|
|
232
|
+
elif condition.operator == Operator.GTE:
|
|
233
|
+
return value >= condition.value
|
|
234
|
+
elif condition.operator == Operator.LT:
|
|
235
|
+
return value < condition.value
|
|
236
|
+
elif condition.operator == Operator.LTE:
|
|
237
|
+
return value <= condition.value
|
|
238
|
+
elif condition.operator == Operator.CONTAINS:
|
|
239
|
+
return str(condition.value).lower() in str(value).lower()
|
|
240
|
+
elif condition.operator == Operator.CONTAINS_CS:
|
|
241
|
+
return str(condition.value) in str(value)
|
|
242
|
+
elif condition.operator == Operator.IN:
|
|
243
|
+
return value in condition.values # type: ignore
|
|
244
|
+
elif condition.operator == Operator.NOT_IN:
|
|
245
|
+
return value not in condition.values # type: ignore
|
|
246
|
+
elif condition.operator == Operator.IS_NULL:
|
|
247
|
+
return value is None
|
|
248
|
+
elif condition.operator == Operator.IS_NOT_NULL:
|
|
249
|
+
return value is not None
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
def _evaluate_where_clause(self, row: Dict[str, Any], where_clause: WhereClause) -> bool:
|
|
253
|
+
"""Evaluate a where clause against a row"""
|
|
254
|
+
results = [self._evaluate_condition(row, condition) for condition in where_clause.conditions]
|
|
255
|
+
if where_clause.operator == "AND":
|
|
256
|
+
return all(results)
|
|
257
|
+
return any(results)
|
|
258
|
+
|
|
259
|
+
async def get(self, query: SelectQuery) -> List[Dict[str, Any]]:
|
|
260
|
+
"""Get records from the database"""
|
|
261
|
+
self._assert_connection()
|
|
262
|
+
|
|
263
|
+
# Get schema
|
|
264
|
+
schema_key = f"{self.SCHEMA_PREFIX}{query.table}"
|
|
265
|
+
schema_json = await self._db.get(schema_key)
|
|
266
|
+
if not schema_json:
|
|
267
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
268
|
+
|
|
269
|
+
schema = TableSchema.model_validate(json.loads(schema_json))
|
|
270
|
+
|
|
271
|
+
# Get all rows from the table
|
|
272
|
+
table_key = f"{self.TABLE_PREFIX}{query.table}"
|
|
273
|
+
all_rows = await self._db.hgetall(table_key) # type: ignore
|
|
274
|
+
|
|
275
|
+
rows = []
|
|
276
|
+
for row_id, row_json in all_rows.items():
|
|
277
|
+
row_data = json.loads(row_json)
|
|
278
|
+
|
|
279
|
+
# Convert string values back to appropriate types
|
|
280
|
+
processed_row = {}
|
|
281
|
+
for col in schema.columns:
|
|
282
|
+
if col.name in row_data:
|
|
283
|
+
value = row_data[col.name]
|
|
284
|
+
# Try to convert back to original type
|
|
285
|
+
if col.type in [ColumnType.INTEGER, ColumnType.BIGINT, ColumnType.AUTOINCREMENT]:
|
|
286
|
+
try:
|
|
287
|
+
processed_row[col.name] = int(value)
|
|
288
|
+
except (ValueError, TypeError):
|
|
289
|
+
processed_row[col.name] = value
|
|
290
|
+
elif col.type in [ColumnType.FLOAT, ColumnType.DOUBLE, ColumnType.DECIMAL]:
|
|
291
|
+
try:
|
|
292
|
+
processed_row[col.name] = float(value) # type: ignore
|
|
293
|
+
except (ValueError, TypeError):
|
|
294
|
+
processed_row[col.name] = value
|
|
295
|
+
elif col.type == ColumnType.BOOLEAN:
|
|
296
|
+
processed_row[col.name] = value.lower() in ('true', '1', 'yes')
|
|
297
|
+
elif col.type == ColumnType.JSON:
|
|
298
|
+
try:
|
|
299
|
+
processed_row[col.name] = json.loads(value)
|
|
300
|
+
except (ValueError, TypeError):
|
|
301
|
+
processed_row[col.name] = value
|
|
302
|
+
else:
|
|
303
|
+
processed_row[col.name] = value
|
|
304
|
+
|
|
305
|
+
rows.append(processed_row)
|
|
306
|
+
|
|
307
|
+
# Apply where clause if present
|
|
308
|
+
if query.where:
|
|
309
|
+
column_names = set(column.name for column in schema.columns)
|
|
310
|
+
for condition in query.where.conditions:
|
|
311
|
+
if condition.column not in column_names:
|
|
312
|
+
raise DBQueryError(f"Condition on invalid column '{condition.column}'")
|
|
313
|
+
rows = [row for row in rows if self._evaluate_where_clause(row, query.where)]
|
|
314
|
+
|
|
315
|
+
# Apply order by if present
|
|
316
|
+
if query.order_by:
|
|
317
|
+
for order in reversed(query.order_by):
|
|
318
|
+
rows.sort(key=lambda x: x.get(order.column, ''), reverse=order.direction == "DESC")
|
|
319
|
+
|
|
320
|
+
# Apply limit and offset if present
|
|
321
|
+
if query.offset:
|
|
322
|
+
rows = rows[query.offset:]
|
|
323
|
+
if query.limit:
|
|
324
|
+
rows = rows[:query.limit]
|
|
325
|
+
|
|
326
|
+
return rows
|
|
327
|
+
|
|
328
|
+
async def update(self, query: UpdateQuery) -> int:
|
|
329
|
+
"""Update records in the database"""
|
|
330
|
+
self._assert_connection()
|
|
331
|
+
|
|
332
|
+
# Get schema
|
|
333
|
+
schema_key = f"{self.SCHEMA_PREFIX}{query.table}"
|
|
334
|
+
schema_json = await self._db.get(schema_key)
|
|
335
|
+
if not schema_json:
|
|
336
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
337
|
+
|
|
338
|
+
schema = TableSchema.model_validate(json.loads(schema_json))
|
|
339
|
+
updated_count = 0
|
|
340
|
+
|
|
341
|
+
# Get all rows from the table
|
|
342
|
+
table_key = f"{self.TABLE_PREFIX}{query.table}"
|
|
343
|
+
all_rows = await self._db.hgetall(table_key) # type: ignore
|
|
344
|
+
|
|
345
|
+
# Validate new values against schema
|
|
346
|
+
for column_name, value in query.data.items():
|
|
347
|
+
column = next((col for col in schema.columns if col.name == column_name), None)
|
|
348
|
+
if not column:
|
|
349
|
+
raise ValueError(f"Column '{column_name}' does not exist")
|
|
350
|
+
if not self._validate_column_type(column, value):
|
|
351
|
+
raise ValueError(f"Invalid type for column '{column_name}'")
|
|
352
|
+
|
|
353
|
+
# Update matching rows
|
|
354
|
+
for row_id, row_json in all_rows.items():
|
|
355
|
+
row_data = json.loads(row_json)
|
|
356
|
+
|
|
357
|
+
if not query.where or self._evaluate_where_clause(row_data, query.where):
|
|
358
|
+
# Update row data
|
|
359
|
+
for key, value in query.data.items():
|
|
360
|
+
if isinstance(value, (dict, list)):
|
|
361
|
+
row_data[key] = json.dumps(value)
|
|
362
|
+
else:
|
|
363
|
+
row_data[key] = str(value)
|
|
364
|
+
|
|
365
|
+
# Store updated row
|
|
366
|
+
await self._db.hset(table_key, row_id, json.dumps(row_data)) # type: ignore
|
|
367
|
+
updated_count += 1
|
|
368
|
+
|
|
369
|
+
self.logger.info("Updated %s rows in '%s'", updated_count, query.table)
|
|
370
|
+
return updated_count
|
|
371
|
+
|
|
372
|
+
async def delete(self, query: DeleteQuery) -> int:
|
|
373
|
+
"""Delete records from the database"""
|
|
374
|
+
self._assert_connection()
|
|
375
|
+
|
|
376
|
+
# Get schema
|
|
377
|
+
schema_key = f"{self.SCHEMA_PREFIX}{query.table}"
|
|
378
|
+
schema_json = await self._db.get(schema_key)
|
|
379
|
+
if not schema_json:
|
|
380
|
+
raise ValueError(f"Table '{query.table}' does not exist")
|
|
381
|
+
|
|
382
|
+
# Get all rows from the table
|
|
383
|
+
table_key = f"{self.TABLE_PREFIX}{query.table}"
|
|
384
|
+
all_rows = await self._db.hgetall(table_key) # type: ignore
|
|
385
|
+
|
|
386
|
+
# Find rows to delete
|
|
387
|
+
rows_to_delete = []
|
|
388
|
+
for row_id, row_json in all_rows.items():
|
|
389
|
+
row_data = json.loads(row_json)
|
|
390
|
+
if not query.where or self._evaluate_where_clause(row_data, query.where):
|
|
391
|
+
rows_to_delete.append(row_id)
|
|
392
|
+
|
|
393
|
+
# Delete rows
|
|
394
|
+
for row_id in rows_to_delete:
|
|
395
|
+
await self._db.hdel(table_key, row_id) # type: ignore
|
|
396
|
+
|
|
397
|
+
self.logger.info("Deleted %s rows from '%s'", len(rows_to_delete), query.table)
|
|
398
|
+
return len(rows_to_delete)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
__all__ = [
|
|
402
|
+
"RedisDatabase"
|
|
403
|
+
]
|