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,261 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Dict, Optional, Type, Any, Sequence
|
|
4
|
+
from ...logging_.utils import get_logger
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from sqlalchemy import inspect, Column
|
|
8
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
9
|
+
except ImportError:
|
|
10
|
+
from ...mock_ import MockImportObject
|
|
11
|
+
|
|
12
|
+
inspect = MockImportObject("'sqlalchemy' is not installed") # type:ignore
|
|
13
|
+
Column = MockImportObject("'sqlalchemy' is not installed") # type:ignore
|
|
14
|
+
DeclarativeBase = type("DeclarativeBase", (object,), {}) # type:ignore
|
|
15
|
+
|
|
16
|
+
from .database import Database
|
|
17
|
+
from .database_definitions import ColumnType, TableSchema, TableColumn as ColumnSchema, TableIndex as IndexSchema
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
TYPE_MAPPING = {
|
|
21
|
+
'INTEGER': ColumnType.INTEGER,
|
|
22
|
+
'BIGINT': ColumnType.INTEGER,
|
|
23
|
+
'FLOAT': ColumnType.FLOAT,
|
|
24
|
+
'NUMERIC': ColumnType.FLOAT,
|
|
25
|
+
'DECIMAL': ColumnType.FLOAT,
|
|
26
|
+
'BOOLEAN': ColumnType.BOOLEAN,
|
|
27
|
+
'TEXT': ColumnType.TEXT,
|
|
28
|
+
'VARCHAR': ColumnType.TEXT,
|
|
29
|
+
'CHAR': ColumnType.TEXT,
|
|
30
|
+
'UUID': ColumnType.TEXT,
|
|
31
|
+
'DATETIME': ColumnType.DATETIME,
|
|
32
|
+
'DATE': ColumnType.DATE,
|
|
33
|
+
'TIME': ColumnType.TIME,
|
|
34
|
+
'JSON': ColumnType.JSON,
|
|
35
|
+
'BLOB': ColumnType.BLOB,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def validate_schema(db: Database, table_name: str, expected_schema: TableSchema) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Validate that the existing table schema matches the expected schema.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
db: Database instance
|
|
45
|
+
table_name: Name of the table to validate
|
|
46
|
+
expected_schema: Expected table schema
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if schema matches, False otherwise
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
# Get existing schema
|
|
53
|
+
existing_schemas = await db.get_schemas()
|
|
54
|
+
if table_name not in existing_schemas:
|
|
55
|
+
logger.warning("Table '%s' does not exist", table_name)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
existing_schema = existing_schemas[table_name]
|
|
59
|
+
|
|
60
|
+
# Compare columns
|
|
61
|
+
if len(existing_schema.columns) != len(expected_schema.columns):
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Column count mismatch in '%s': "
|
|
64
|
+
"expected %d, "
|
|
65
|
+
"got %d",
|
|
66
|
+
table_name,
|
|
67
|
+
len(expected_schema.columns),
|
|
68
|
+
len(existing_schema.columns)
|
|
69
|
+
)
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# Compare each column
|
|
73
|
+
for expected_col, existing_col in zip(sorted(expected_schema.columns, key=lambda c: c.name),
|
|
74
|
+
sorted(existing_schema.columns, key=lambda c: c.name)):
|
|
75
|
+
if (
|
|
76
|
+
expected_col.name != existing_col.name or
|
|
77
|
+
expected_col.type != existing_col.type or
|
|
78
|
+
expected_col.primary_key != existing_col.primary_key or
|
|
79
|
+
expected_col.nullable != existing_col.nullable or
|
|
80
|
+
expected_col.unique != existing_col.unique or
|
|
81
|
+
expected_col.foreign_key != existing_col.foreign_key
|
|
82
|
+
):
|
|
83
|
+
logger.warning("Column mismatch in '%s': expected %s, got %s", table_name, expected_col, existing_col)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Compare indexes
|
|
87
|
+
if len(existing_schema.indexes) != len(expected_schema.indexes):
|
|
88
|
+
logger.warning(
|
|
89
|
+
"Index count mismatch in '%s': "
|
|
90
|
+
"expected %d, "
|
|
91
|
+
"got %d",
|
|
92
|
+
table_name,
|
|
93
|
+
len(expected_schema.indexes),
|
|
94
|
+
len(existing_schema.indexes)
|
|
95
|
+
)
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# Compare each index
|
|
99
|
+
for expected_idx, existing_idx in zip(sorted(expected_schema.indexes, key=lambda idx: idx.name),
|
|
100
|
+
sorted(existing_schema.indexes, key=lambda idx: idx.name)):
|
|
101
|
+
if (
|
|
102
|
+
expected_idx.name != existing_idx.name or
|
|
103
|
+
expected_idx.columns != existing_idx.columns or
|
|
104
|
+
expected_idx.unique != existing_idx.unique
|
|
105
|
+
):
|
|
106
|
+
logger.warning("Index mismatch in '%s': expected %s, got %s", table_name, expected_idx, existing_idx)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error("Error validating schema for '%s': %s", table_name, str(e))
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_column_type(column: Column) -> ColumnType:
|
|
117
|
+
"""Convert SQLAlchemy column type to our ColumnType enum"""
|
|
118
|
+
# Ensure autoincrement for integer primary key named 'id'
|
|
119
|
+
if (
|
|
120
|
+
column.name == 'id'
|
|
121
|
+
and getattr(column, 'primary_key', False)
|
|
122
|
+
and ('INTEGER' in str(column.type).upper() or 'BIGINT' in str(column.type).upper())
|
|
123
|
+
):
|
|
124
|
+
return ColumnType.AUTOINCREMENT
|
|
125
|
+
# Check for autoincrement attribute
|
|
126
|
+
if getattr(column, 'autoincrement', False) == True:
|
|
127
|
+
return ColumnType.AUTOINCREMENT
|
|
128
|
+
# Then check the column type
|
|
129
|
+
type_name = str(column.type).upper()
|
|
130
|
+
for sql_type, our_type in TYPE_MAPPING.items():
|
|
131
|
+
if sql_type in type_name:
|
|
132
|
+
return our_type
|
|
133
|
+
return ColumnType.TEXT # Default to TEXT if type not found
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_foreign_key(column: Column) -> Optional[Dict[str, str]]:
|
|
137
|
+
"""Extract foreign key information from a column"""
|
|
138
|
+
for fk in column.foreign_keys:
|
|
139
|
+
return {
|
|
140
|
+
"table": fk.column.table.name,
|
|
141
|
+
"column": fk.column.name
|
|
142
|
+
}
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_default_value(column: Column) -> Optional[Any]:
|
|
147
|
+
"""Extract default value from a column"""
|
|
148
|
+
if column.default is not None:
|
|
149
|
+
if hasattr(column.default, 'arg'):
|
|
150
|
+
return column.default.arg
|
|
151
|
+
return column.default
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def model_to_schema(model_class: Type[DeclarativeBase]) -> TableSchema:
|
|
156
|
+
"""Convert a SQLAlchemy model to our TableSchema"""
|
|
157
|
+
mapper = inspect(model_class)
|
|
158
|
+
table_name = mapper.local_table.name # type: ignore
|
|
159
|
+
|
|
160
|
+
# Convert columns
|
|
161
|
+
columns = []
|
|
162
|
+
for column in mapper.columns:
|
|
163
|
+
# Get unique constraint from column
|
|
164
|
+
is_unique = False
|
|
165
|
+
if column.unique:
|
|
166
|
+
is_unique = True
|
|
167
|
+
elif column.index and column.index.unique: # type: ignore
|
|
168
|
+
is_unique = True
|
|
169
|
+
|
|
170
|
+
col_schema = ColumnSchema(
|
|
171
|
+
name=column.name,
|
|
172
|
+
type=get_column_type(column),
|
|
173
|
+
primary_key=column.primary_key,
|
|
174
|
+
nullable=column.nullable, # type: ignore
|
|
175
|
+
unique=is_unique,
|
|
176
|
+
foreign_key=get_foreign_key(column),
|
|
177
|
+
default=get_default_value(column)
|
|
178
|
+
)
|
|
179
|
+
columns.append(col_schema)
|
|
180
|
+
|
|
181
|
+
# Convert indexes
|
|
182
|
+
indexes = []
|
|
183
|
+
for index in mapper.local_table.indexes: # type: ignore
|
|
184
|
+
# Skip indexes that are already handled by unique constraints
|
|
185
|
+
if len(index.columns) == 1 and index.columns[0].unique:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
idx_schema = IndexSchema(
|
|
189
|
+
name=index.name,
|
|
190
|
+
columns=[col.name for col in index.columns],
|
|
191
|
+
unique=index.unique
|
|
192
|
+
)
|
|
193
|
+
indexes.append(idx_schema)
|
|
194
|
+
|
|
195
|
+
return TableSchema(
|
|
196
|
+
name=table_name,
|
|
197
|
+
columns=columns,
|
|
198
|
+
indexes=indexes
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class DatabaseInitializer(ABC):
|
|
203
|
+
# Map SQLAlchemy types to our ColumnType enum
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
@abstractmethod
|
|
207
|
+
def _get_models(cls) -> Sequence[Type[DeclarativeBase]]:
|
|
208
|
+
"""Get all pydantic models from our models"""
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def _get_table_schemas(cls) -> Dict[str, TableSchema]:
|
|
212
|
+
"""Get all table schemas from our models"""
|
|
213
|
+
models = cls._get_models()
|
|
214
|
+
res = {}
|
|
215
|
+
for model in models:
|
|
216
|
+
try:
|
|
217
|
+
res[model.__tablename__] = model_to_schema(model)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise Exception(f"Failed parsing '{model.__tablename__}'") from e
|
|
220
|
+
return res
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
async def init_db(cls, db: Database) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Initialize the database by creating or validating all required tables.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
db: Database instance to initialize
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
await db.connect()
|
|
232
|
+
|
|
233
|
+
# Get existing schemas
|
|
234
|
+
existing_schemas = await db.get_schemas()
|
|
235
|
+
|
|
236
|
+
# Get table schemas from models
|
|
237
|
+
table_schemas = cls._get_table_schemas()
|
|
238
|
+
|
|
239
|
+
# Create or validate each table
|
|
240
|
+
for table_name, expected_schema in table_schemas.items():
|
|
241
|
+
if table_name not in existing_schemas:
|
|
242
|
+
logger.info("Creating table '%s'", table_name)
|
|
243
|
+
await db.create_table(expected_schema)
|
|
244
|
+
else:
|
|
245
|
+
logger.info("Validating table '%s'", table_name)
|
|
246
|
+
if not await validate_schema(db, table_name, expected_schema):
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Schema validation failed for table '{table_name}'. "
|
|
249
|
+
"Please check the logs for details."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
logger.info("Database initialization completed successfully")
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error("Error initializing database: %s", str(e))
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = [
|
|
260
|
+
"DatabaseInitializer"
|
|
261
|
+
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional, Literal, Union
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ColumnType(str, Enum):
|
|
8
|
+
"""SQL column types"""
|
|
9
|
+
INTEGER = "INTEGER"
|
|
10
|
+
AUTOINCREMENT = "AUTOINCREMENT"
|
|
11
|
+
SMALLINT = "SMALLINT"
|
|
12
|
+
MEDIUMINT = "MEDIUMINT"
|
|
13
|
+
TINYINT = "TINYINT"
|
|
14
|
+
BIGINT = "BIGINT"
|
|
15
|
+
FLOAT = "FLOAT"
|
|
16
|
+
DOUBLE = "DOUBLE"
|
|
17
|
+
DECIMAL = "DECIMAL"
|
|
18
|
+
BOOLEAN = "BOOLEAN"
|
|
19
|
+
TEXT = "TEXT"
|
|
20
|
+
VARCHAR = "VARCHAR"
|
|
21
|
+
CHAR = "CHAR"
|
|
22
|
+
DATE = "DATE"
|
|
23
|
+
TIME = "TIME"
|
|
24
|
+
DATETIME = "DATETIME"
|
|
25
|
+
TIMESTAMP = "TIMESTAMP"
|
|
26
|
+
BLOB = "BLOB"
|
|
27
|
+
JSON = "JSON"
|
|
28
|
+
UUID = "UUID"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseDBModel(BaseModel):
|
|
32
|
+
"""Base model with JSON serialization support"""
|
|
33
|
+
model_config = ConfigDict(
|
|
34
|
+
arbitrary_types_allowed=True
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
"""Convert model to dictionary"""
|
|
39
|
+
return self.model_dump(mode="json")
|
|
40
|
+
|
|
41
|
+
def to_json(self) -> str:
|
|
42
|
+
"""Convert model to JSON string"""
|
|
43
|
+
return self.model_dump_json()
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_json(cls, json_str: str) -> 'BaseDBModel':
|
|
47
|
+
"""Create model instance from JSON string"""
|
|
48
|
+
data = json.loads(json_str)
|
|
49
|
+
return cls.model_validate(data)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TableColumn(BaseDBModel):
|
|
53
|
+
"""Represents a database column definition"""
|
|
54
|
+
name: str
|
|
55
|
+
type: ColumnType
|
|
56
|
+
nullable: bool = True
|
|
57
|
+
primary_key: bool = False
|
|
58
|
+
unique: bool = False
|
|
59
|
+
default: Any = None
|
|
60
|
+
foreign_key: Optional[Dict[str, str]] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TableIndex(BaseDBModel):
|
|
64
|
+
"""Represents a database index definition"""
|
|
65
|
+
name: str
|
|
66
|
+
columns: List[str]
|
|
67
|
+
unique: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TableForeignKey(BaseDBModel):
|
|
71
|
+
"""Represents a foreign key constraint"""
|
|
72
|
+
name: str
|
|
73
|
+
columns: List[str]
|
|
74
|
+
reference_table: str
|
|
75
|
+
reference_columns: List[str]
|
|
76
|
+
on_delete: Optional[str] = None
|
|
77
|
+
on_update: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TableSchema(BaseDBModel):
|
|
81
|
+
"""Represents a complete table schema"""
|
|
82
|
+
name: str
|
|
83
|
+
columns: List[TableColumn]
|
|
84
|
+
indexes: List[TableIndex] = Field(default_factory=list)
|
|
85
|
+
foreign_keys: List[TableForeignKey] = Field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Operator(str, Enum):
|
|
89
|
+
"""SQL comparison operators"""
|
|
90
|
+
EQ = "="
|
|
91
|
+
NEQ = "!="
|
|
92
|
+
GT = ">"
|
|
93
|
+
GTE = ">="
|
|
94
|
+
LT = "<"
|
|
95
|
+
LTE = "<="
|
|
96
|
+
LIKE = "LIKE" # Not supported in InMemoryDatabase
|
|
97
|
+
ILIKE = "ILIKE" # Not supported in InMemoryDatabase
|
|
98
|
+
CONTAINS = "CONTAINS" # Case-insensitive contains
|
|
99
|
+
CONTAINS_CS = "CONTAINS_CS" # Case-sensitive contains
|
|
100
|
+
IN = "IN"
|
|
101
|
+
NOT_IN = "NOT IN"
|
|
102
|
+
IS_NULL = "IS NULL"
|
|
103
|
+
IS_NOT_NULL = "IS NOT NULL"
|
|
104
|
+
AND = "AND"
|
|
105
|
+
OR = "OR"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class JoinType(str, Enum):
|
|
109
|
+
"""SQL join types"""
|
|
110
|
+
INNER = "INNER JOIN"
|
|
111
|
+
LEFT = "LEFT JOIN"
|
|
112
|
+
RIGHT = "RIGHT JOIN"
|
|
113
|
+
FULL = "FULL JOIN"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class OrderDirection(str, Enum):
|
|
117
|
+
"""SQL order directions"""
|
|
118
|
+
ASC = "ASC"
|
|
119
|
+
DESC = "DESC"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Condition(BaseDBModel):
|
|
123
|
+
"""A single SQL condition"""
|
|
124
|
+
column: str
|
|
125
|
+
operator: Operator
|
|
126
|
+
value: Optional[Any] = None
|
|
127
|
+
values: Optional[List[Any]] = None # For IN/NOT IN operators
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Join(BaseDBModel):
|
|
131
|
+
"""SQL join definition"""
|
|
132
|
+
type: JoinType
|
|
133
|
+
table: str
|
|
134
|
+
conditions: List[Condition]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class OrderBy(BaseDBModel):
|
|
138
|
+
"""SQL ORDER BY clause"""
|
|
139
|
+
column: str
|
|
140
|
+
direction: OrderDirection = OrderDirection.ASC
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WhereClause(BaseDBModel):
|
|
144
|
+
"""SQL WHERE clause with conditions"""
|
|
145
|
+
conditions: List[Condition]
|
|
146
|
+
operator: Literal["AND", "OR"] = "AND"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class SelectQuery(BaseDBModel):
|
|
150
|
+
"""Complete SELECT query definition"""
|
|
151
|
+
table: str
|
|
152
|
+
columns: Optional[List[str]] = None
|
|
153
|
+
where: Optional[WhereClause] = None
|
|
154
|
+
joins: Optional[List[Join]] = None
|
|
155
|
+
order_by: Optional[List[OrderBy]] = None
|
|
156
|
+
group_by: Optional[List[str]] = None
|
|
157
|
+
having: Optional[WhereClause] = None
|
|
158
|
+
limit: Optional[int] = None
|
|
159
|
+
offset: Optional[int] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class UpdateQuery(BaseDBModel):
|
|
163
|
+
"""Complete UPDATE query definition"""
|
|
164
|
+
table: str
|
|
165
|
+
data: Dict[str, Any]
|
|
166
|
+
where: Optional[WhereClause] = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class DeleteQuery(BaseDBModel):
|
|
170
|
+
"""Complete DELETE query definition"""
|
|
171
|
+
table: str
|
|
172
|
+
where: Optional[WhereClause] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Helper functions for JSON serialization
|
|
176
|
+
def serialize_to_json(obj: Union[BaseDBModel, List[BaseDBModel], Dict[str, Any]]) -> str:
|
|
177
|
+
"""Serialize any supported object to JSON string"""
|
|
178
|
+
if isinstance(obj, BaseDBModel):
|
|
179
|
+
return obj.to_json()
|
|
180
|
+
elif isinstance(obj, list):
|
|
181
|
+
return json.dumps([item.to_dict() if isinstance(item, BaseDBModel) else item for item in obj])
|
|
182
|
+
elif isinstance(obj, dict):
|
|
183
|
+
return json.dumps({k: v.to_dict() if isinstance(v, BaseDBModel) else v for k, v in obj.items()})
|
|
184
|
+
return json.dumps(obj)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def deserialize_from_json(json_str: str, model_class: type[BaseDBModel]) -> Union[BaseDBModel, List[BaseDBModel]]:
|
|
188
|
+
"""Deserialize JSON string to model instance(s)"""
|
|
189
|
+
data = json.loads(json_str)
|
|
190
|
+
if isinstance(data, list):
|
|
191
|
+
return [model_class.model_validate(item) for item in data]
|
|
192
|
+
return model_class.model_validate(data)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"ColumnType",
|
|
197
|
+
"TableColumn",
|
|
198
|
+
"TableIndex",
|
|
199
|
+
"TableForeignKey",
|
|
200
|
+
"TableSchema",
|
|
201
|
+
"Operator",
|
|
202
|
+
"JoinType",
|
|
203
|
+
"OrderDirection",
|
|
204
|
+
"Condition",
|
|
205
|
+
"Join",
|
|
206
|
+
"OrderBy",
|
|
207
|
+
"WhereClause",
|
|
208
|
+
"SelectQuery",
|
|
209
|
+
"UpdateQuery",
|
|
210
|
+
"DeleteQuery",
|
|
211
|
+
"serialize_to_json",
|
|
212
|
+
"deserialize_from_json"
|
|
213
|
+
]
|