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.
Files changed (287) hide show
  1. danielutils/__init__.py +50 -0
  2. danielutils/abstractions/__init__.py +3 -0
  3. danielutils/abstractions/db/__init__.py +10 -0
  4. danielutils/abstractions/db/database.py +206 -0
  5. danielutils/abstractions/db/database_definitions.py +219 -0
  6. danielutils/abstractions/db/database_exceptions.py +34 -0
  7. danielutils/abstractions/db/database_factory.py +70 -0
  8. danielutils/abstractions/db/database_initializer.py +261 -0
  9. danielutils/abstractions/db/database_models.py +213 -0
  10. danielutils/abstractions/db/dependencies.py +6 -0
  11. danielutils/abstractions/db/implementations/__init__.py +4 -0
  12. danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
  13. danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
  14. danielutils/abstractions/db/implementations/redis_database.py +403 -0
  15. danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
  16. danielutils/abstractions/multiprogramming/__init__.py +3 -0
  17. danielutils/abstractions/multiprogramming/multi_id.py +26 -0
  18. danielutils/abstractions/multiprogramming/worker.py +104 -0
  19. danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
  20. danielutils/abstractions/repl.py +125 -0
  21. danielutils/aliases.py +174 -0
  22. danielutils/async_/__init__.py +7 -0
  23. danielutils/async_/async_cmd.py +28 -0
  24. danielutils/async_/async_command.py +879 -0
  25. danielutils/async_/async_layered_command.py +125 -0
  26. danielutils/async_/async_retry_executor.py +86 -0
  27. danielutils/async_/async_worker_pool.py +110 -0
  28. danielutils/async_/time_strategy.py +73 -0
  29. danielutils/async_/utils.py +57 -0
  30. danielutils/better_builtins/__init__.py +3 -0
  31. danielutils/better_builtins/counter.py +48 -0
  32. danielutils/better_builtins/frange.py +266 -0
  33. danielutils/better_builtins/typed_builtins/__init__.py +4 -0
  34. danielutils/better_builtins/typed_builtins/factory.py +126 -0
  35. danielutils/better_builtins/typed_builtins/tdict.py +36 -0
  36. danielutils/better_builtins/typed_builtins/tlist.py +223 -0
  37. danielutils/better_builtins/typed_builtins/tset.py +67 -0
  38. danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
  39. danielutils/colors.py +203 -0
  40. danielutils/context_managers/__init__.py +5 -0
  41. danielutils/context_managers/attr_context.py +40 -0
  42. danielutils/context_managers/multi_context.py +34 -0
  43. danielutils/context_managers/optional_context.py +33 -0
  44. danielutils/context_managers/state_context.py +25 -0
  45. danielutils/context_managers/temporary_file.py +108 -0
  46. danielutils/convenience.py +12 -0
  47. danielutils/conversions/__init__.py +2 -0
  48. danielutils/conversions/main_conversions.py +131 -0
  49. danielutils/conversions/specialized_conversions/__init__.py +2 -0
  50. danielutils/conversions/specialized_conversions/to_hex.py +25 -0
  51. danielutils/conversions/specialized_conversions/to_int.py +19 -0
  52. danielutils/custom_types.py +18 -0
  53. danielutils/data_structures/__init__.py +8 -0
  54. danielutils/data_structures/algorithms.py +52 -0
  55. danielutils/data_structures/comparer.py +47 -0
  56. danielutils/data_structures/default_dict.py +23 -0
  57. danielutils/data_structures/functions.py +35 -0
  58. danielutils/data_structures/graph/__init__.py +4 -0
  59. danielutils/data_structures/graph/binary_node.py +71 -0
  60. danielutils/data_structures/graph/graph.py +208 -0
  61. danielutils/data_structures/graph/multinode.py +101 -0
  62. danielutils/data_structures/graph/node.py +60 -0
  63. danielutils/data_structures/heap/__init__.py +3 -0
  64. danielutils/data_structures/heap/heap.py +119 -0
  65. danielutils/data_structures/heap/max_heap.py +18 -0
  66. danielutils/data_structures/heap/min_heap.py +18 -0
  67. danielutils/data_structures/queue/__init__.py +3 -0
  68. danielutils/data_structures/queue/atomic_queue.py +15 -0
  69. danielutils/data_structures/queue/priority_queue.py +110 -0
  70. danielutils/data_structures/queue/queue.py +92 -0
  71. danielutils/data_structures/stack.py +102 -0
  72. danielutils/data_structures/trees/__init__.py +2 -0
  73. danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
  74. danielutils/data_structures/trees/binary_tree.py +63 -0
  75. danielutils/date.py +43 -0
  76. danielutils/date_time.py +23 -0
  77. danielutils/decorators/__init__.py +18 -0
  78. danielutils/decorators/atomic.py +45 -0
  79. danielutils/decorators/attach.py +61 -0
  80. danielutils/decorators/chain_decorators.py +39 -0
  81. danielutils/decorators/decorate_conditionally.py +32 -0
  82. danielutils/decorators/delay_call.py +47 -0
  83. danielutils/decorators/deprecate.py +46 -0
  84. danielutils/decorators/final.py +39 -0
  85. danielutils/decorators/limit_recursion.py +63 -0
  86. danielutils/decorators/memo.py +68 -0
  87. danielutils/decorators/normalize_decorator.py +55 -0
  88. danielutils/decorators/overload.py +204 -0
  89. danielutils/decorators/partially_implemented.py +32 -0
  90. danielutils/decorators/processify.py +74 -0
  91. danielutils/decorators/property.py +42 -0
  92. danielutils/decorators/singleton.py +59 -0
  93. danielutils/decorators/threadify.py +40 -0
  94. danielutils/decorators/timeout.py +82 -0
  95. danielutils/decorators/total_ordering.py +82 -0
  96. danielutils/decorators/validate.py +227 -0
  97. danielutils/exceptions.py +90 -0
  98. danielutils/functions/__init__.py +13 -0
  99. danielutils/functions/areoneof.py +47 -0
  100. danielutils/functions/check_foreach.py +39 -0
  101. danielutils/functions/factorial.py +24 -0
  102. danielutils/functions/flatten.py +33 -0
  103. danielutils/functions/foreach.py +20 -0
  104. danielutils/functions/isoftype.py +475 -0
  105. danielutils/functions/isoneof.py +71 -0
  106. danielutils/functions/multiloop.py +51 -0
  107. danielutils/functions/parallel_for.py +47 -0
  108. danielutils/functions/partition.py +45 -0
  109. danielutils/functions/powerset.py +31 -0
  110. danielutils/functions/subseteq.py +25 -0
  111. danielutils/functions/types_subseteq.py +51 -0
  112. danielutils/generators/__init__.py +3 -0
  113. danielutils/generators/conditional_generator.py +59 -0
  114. danielutils/generators/generator_from_stream.py +31 -0
  115. danielutils/generators/join_generators.py +109 -0
  116. danielutils/internet.py +92 -0
  117. danielutils/io_.py +451 -0
  118. danielutils/java/__init__.py +18 -0
  119. danielutils/java/interfaces/__init__.py +1 -0
  120. danielutils/java/interfaces/comparable.py +31 -0
  121. danielutils/java/java_interface.py +59 -0
  122. danielutils/logging_/__init__.py +2 -0
  123. danielutils/logging_/_impl/__init__.py +4 -0
  124. danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
  125. danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
  126. danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
  127. danielutils/logging_/_impl/log_level.py +13 -0
  128. danielutils/logging_/_impl/logger.py +109 -0
  129. danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
  130. danielutils/logging_/utils.py +199 -0
  131. danielutils/lombok/__init__.py +1 -0
  132. danielutils/lombok/builder.py +68 -0
  133. danielutils/math_/__init__.py +2 -0
  134. danielutils/math_/constants.py +103 -0
  135. danielutils/math_/functions.py +34 -0
  136. danielutils/math_/math_print.py +52 -0
  137. danielutils/math_/math_symbols.py +125 -0
  138. danielutils/metaclasses/__init__.py +5 -0
  139. danielutils/metaclasses/atomic_class_meta.py +36 -0
  140. danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
  141. danielutils/metaclasses/instance_cache_meta.py +66 -0
  142. danielutils/metaclasses/interface.py +328 -0
  143. danielutils/metaclasses/overload_meta.py +48 -0
  144. danielutils/mock_/__init__.py +1 -0
  145. danielutils/mock_/mock_module.py +27 -0
  146. danielutils/path.py +55 -0
  147. danielutils/print_.py +123 -0
  148. danielutils/progress_bar/__init__.py +3 -0
  149. danielutils/progress_bar/ascii_progress_bar.py +161 -0
  150. danielutils/progress_bar/progress_bar.py +75 -0
  151. danielutils/progress_bar/progress_bar_pool.py +72 -0
  152. danielutils/protocols/__init__.py +3 -0
  153. danielutils/protocols/dictable.py +38 -0
  154. danielutils/protocols/evaluable.py +26 -0
  155. danielutils/protocols/serializable.py +51 -0
  156. danielutils/py.typed +0 -0
  157. danielutils/random_.py +47 -0
  158. danielutils/reflection/__init__.py +6 -0
  159. danielutils/reflection/declarations.py +288 -0
  160. danielutils/reflection/file/__init__.py +1 -0
  161. danielutils/reflection/file/file_reflection.py +85 -0
  162. danielutils/reflection/function/__init__.py +1 -0
  163. danielutils/reflection/function/function_reflections.py +152 -0
  164. danielutils/reflection/info_classes/__init__.py +6 -0
  165. danielutils/reflection/info_classes/argument_info.py +278 -0
  166. danielutils/reflection/info_classes/class_info.py +204 -0
  167. danielutils/reflection/info_classes/decorator_info.py +54 -0
  168. danielutils/reflection/info_classes/file_info.py +1180 -0
  169. danielutils/reflection/info_classes/folder_info.py +592 -0
  170. danielutils/reflection/info_classes/function_info.py +551 -0
  171. danielutils/reflection/info_classes/import_info.py +207 -0
  172. danielutils/reflection/interpreter/__init__.py +9 -0
  173. danielutils/reflection/interpreter/callstack.py +57 -0
  174. danielutils/reflection/interpreter/get_traceback.py +27 -0
  175. danielutils/reflection/interpreter/interpreter.py +41 -0
  176. danielutils/reflection/interpreter/is_debugging.py +13 -0
  177. danielutils/reflection/interpreter/os_.py +33 -0
  178. danielutils/reflection/interpreter/packages.py +33 -0
  179. danielutils/reflection/interpreter/python_version.py +33 -0
  180. danielutils/reflection/interpreter/signals.py +18 -0
  181. danielutils/reflection/interpreter/tracer.py +175 -0
  182. danielutils/reflection/module/__init__.py +3 -0
  183. danielutils/reflection/module/lazy_module.py +104 -0
  184. danielutils/reflection/module/module_reflections.py +34 -0
  185. danielutils/reflection/module/package_reflection.py +161 -0
  186. danielutils/retry_executor/__init__.py +3 -0
  187. danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
  188. danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
  189. danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
  190. danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
  191. danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
  192. danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
  193. danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
  194. danielutils/retry_executor/backoff_strategy.py +28 -0
  195. danielutils/retry_executor/retry_executor.py +55 -0
  196. danielutils/snippets/__init__.py +1 -0
  197. danielutils/snippets/try_get.py +28 -0
  198. danielutils/system/__init__.py +3 -0
  199. danielutils/system/independent.py +197 -0
  200. danielutils/system/layered_command.py +130 -0
  201. danielutils/system/windows/__init__.py +2 -0
  202. danielutils/system/windows/utils/__init__.py +1 -0
  203. danielutils/system/windows/utils/filetime.py +19 -0
  204. danielutils/system/windows/win32_ctime.py +183 -0
  205. danielutils/system/windows/windows.py +78 -0
  206. danielutils/testing/__init__.py +1 -0
  207. danielutils/testing/unittest_/__init__.py +2 -0
  208. danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
  209. danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
  210. danielutils/text.py +169 -0
  211. danielutils/time.py +64 -0
  212. danielutils/tools/__init__.py +1 -0
  213. danielutils/tools/unittest_test_runner/__init__.py +9 -0
  214. danielutils/tools/unittest_test_runner/cli.py +70 -0
  215. danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
  216. danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
  217. danielutils/tools/unittest_test_runner/core/execution.py +244 -0
  218. danielutils/tools/unittest_test_runner/core/parser.py +163 -0
  219. danielutils/tools/unittest_test_runner/core/runner.py +561 -0
  220. danielutils/tools/unittest_test_runner/models.py +82 -0
  221. danielutils/university/__init__.py +7 -0
  222. danielutils/university/computability_and_complexity/__init__.py +2 -0
  223. danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
  224. danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
  225. danielutils/university/computability_and_complexity/languages/language.py +17 -0
  226. danielutils/university/computability_and_complexity/languages/sat.py +71 -0
  227. danielutils/university/computability_and_complexity/turing_machine.py +47 -0
  228. danielutils/university/databases/__init__.py +1 -0
  229. danielutils/university/databases/all.py +756 -0
  230. danielutils/university/image_proccesing/__init__.py +1 -0
  231. danielutils/university/image_proccesing/encoding/__init__.py +3 -0
  232. danielutils/university/image_proccesing/encoding/encoding.py +35 -0
  233. danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
  234. danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
  235. danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
  236. danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
  237. danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
  238. danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
  239. danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
  240. danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
  241. danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
  242. danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
  243. danielutils/university/image_proccesing/tansformations/hough.py +0 -0
  244. danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
  245. danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
  246. danielutils/university/linear_algebra/__init__.py +1 -0
  247. danielutils/university/linear_algebra/matrix.py +404 -0
  248. danielutils/university/machine_learning/__init__.py +0 -0
  249. danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
  250. danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
  251. danielutils/university/machine_learning/activation_functions/relu.py +11 -0
  252. danielutils/university/machine_learning/neuron.py +15 -0
  253. danielutils/university/oop/__init__.py +1 -0
  254. danielutils/university/oop/observer.py +31 -0
  255. danielutils/university/oop/strategy.py +9 -0
  256. danielutils/university/probability/__init__.py +7 -0
  257. danielutils/university/probability/conditional_variable/__init__.py +3 -0
  258. danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
  259. danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
  260. danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
  261. danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
  262. danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
  263. danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
  264. danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
  265. danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
  266. danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
  267. danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
  268. danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
  269. danielutils/university/probability/distributions.py +35 -0
  270. danielutils/university/probability/expressions/__init__.py +2 -0
  271. danielutils/university/probability/expressions/accumulation_expression.py +291 -0
  272. danielutils/university/probability/expressions/probability_expression.py +110 -0
  273. danielutils/university/probability/funcs/__init__.py +4 -0
  274. danielutils/university/probability/funcs/covariance.py +12 -0
  275. danielutils/university/probability/funcs/expected_value.py +41 -0
  276. danielutils/university/probability/funcs/probability_function.py +14 -0
  277. danielutils/university/probability/funcs/variance.py +17 -0
  278. danielutils/university/probability/operator.py +79 -0
  279. danielutils/university/probability/protocols.py +32 -0
  280. danielutils/university/probability/supp.py +101 -0
  281. danielutils/university/probability/transformation.py +44 -0
  282. danielutils/versioned_imports.py +31 -0
  283. danielutils-1.1.28.dist-info/METADATA +390 -0
  284. danielutils-1.1.28.dist-info/RECORD +287 -0
  285. danielutils-1.1.28.dist-info/WHEEL +5 -0
  286. danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
  287. 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
+ ]