velocity-python 0.0.131__py3-none-any.whl → 0.0.134__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.
Potentially problematic release.
This version of velocity-python might be problematic. Click here for more details.
- velocity/__init__.py +1 -1
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/core/decorators.py +20 -3
- velocity/db/core/engine.py +33 -7
- velocity/db/exceptions.py +7 -0
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +569 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +52 -2
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +4 -3
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +530 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +625 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +212 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/METADATA +1 -1
- velocity_python-0.0.134.dist-info/RECORD +125 -0
- velocity/db/servers/mysql.py +0 -640
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.131.dist-info/RECORD +0 -62
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.131.dist-info → velocity_python-0.0.134.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import sys
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
# Mock the support module before importing the module that depends on it
|
|
6
|
+
sys.modules["support"] = MagicMock()
|
|
7
|
+
sys.modules["support.app"] = MagicMock()
|
|
8
|
+
sys.modules["support.app"].DEBUG = True
|
|
9
|
+
|
|
10
|
+
from velocity.aws.handlers.response import Response # Replace with actual module path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestResponse(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.response = Response()
|
|
17
|
+
|
|
18
|
+
def test_initial_status_code(self):
|
|
19
|
+
self.assertEqual(self.response.status(), 200)
|
|
20
|
+
|
|
21
|
+
def test_set_status_code(self):
|
|
22
|
+
self.response.set_status(404)
|
|
23
|
+
self.assertEqual(self.response.status(), 404)
|
|
24
|
+
|
|
25
|
+
def test_initial_headers(self):
|
|
26
|
+
headers = self.response.headers()
|
|
27
|
+
self.assertIn("Content-Type", headers)
|
|
28
|
+
self.assertEqual(headers["Content-Type"], "application/json")
|
|
29
|
+
|
|
30
|
+
def test_set_headers(self):
|
|
31
|
+
custom_headers = {"x-custom-header": "value"}
|
|
32
|
+
self.response.set_headers(custom_headers)
|
|
33
|
+
headers = self.response.headers()
|
|
34
|
+
self.assertEqual(headers["X-Custom-Header"], "value") # Ensures capitalization
|
|
35
|
+
self.assertIn("Content-Type", headers)
|
|
36
|
+
|
|
37
|
+
def test_alert_action(self):
|
|
38
|
+
self.response.alert("Test message", "Alert Title")
|
|
39
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
40
|
+
self.assertEqual(self.response.actions[0]["action"], "alert")
|
|
41
|
+
self.assertEqual(self.response.actions[0]["payload"]["title"], "Alert Title")
|
|
42
|
+
self.assertEqual(self.response.actions[0]["payload"]["message"], "Test message")
|
|
43
|
+
|
|
44
|
+
def test_toast_action_valid_variant(self):
|
|
45
|
+
self.response.toast("Toast message", "warning")
|
|
46
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
47
|
+
self.assertEqual(self.response.actions[0]["action"], "toast")
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
self.response.actions[0]["payload"]["options"]["variant"], "warning"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_toast_action_invalid_variant(self):
|
|
53
|
+
with self.assertRaises(ValueError) as context:
|
|
54
|
+
self.response.toast("Invalid toast", "invalid_variant")
|
|
55
|
+
self.assertIn(
|
|
56
|
+
"Notistack variant 'invalid_variant' not in", str(context.exception)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_load_object_action(self):
|
|
60
|
+
payload = {"key": "value"}
|
|
61
|
+
self.response.load_object(payload)
|
|
62
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
63
|
+
self.assertEqual(self.response.actions[0]["action"], "load-object")
|
|
64
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
65
|
+
|
|
66
|
+
def test_update_store_action(self):
|
|
67
|
+
payload = {"key": "value"}
|
|
68
|
+
self.response.update_store(payload)
|
|
69
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
70
|
+
self.assertEqual(self.response.actions[0]["action"], "update-store")
|
|
71
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
72
|
+
|
|
73
|
+
def test_file_download_action(self):
|
|
74
|
+
payload = {"file": "file.txt"}
|
|
75
|
+
self.response.file_download(payload)
|
|
76
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
77
|
+
self.assertEqual(self.response.actions[0]["action"], "file-download")
|
|
78
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
79
|
+
|
|
80
|
+
def test_redirect_action(self):
|
|
81
|
+
self.response.redirect("https://example.com")
|
|
82
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
83
|
+
self.assertEqual(self.response.actions[0]["action"], "redirect")
|
|
84
|
+
self.assertEqual(
|
|
85
|
+
self.response.actions[0]["payload"]["location"], "https://example.com"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def test_signout_action(self):
|
|
89
|
+
self.response.signout()
|
|
90
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
91
|
+
self.assertEqual(self.response.actions[0]["action"], "signout")
|
|
92
|
+
|
|
93
|
+
def test_set_table_action(self):
|
|
94
|
+
payload = {"table": "data"}
|
|
95
|
+
self.response.set_table(payload)
|
|
96
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
97
|
+
self.assertEqual(self.response.actions[0]["action"], "set-table")
|
|
98
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
99
|
+
|
|
100
|
+
def test_set_repo_action(self):
|
|
101
|
+
payload = {"repo": "data"}
|
|
102
|
+
self.response.set_repo(payload)
|
|
103
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
104
|
+
self.assertEqual(self.response.actions[0]["action"], "set-repo")
|
|
105
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
106
|
+
|
|
107
|
+
def test_exception_handling_debug_on(self):
|
|
108
|
+
with patch("your_module.DEBUG", True), patch(
|
|
109
|
+
"traceback.format_exc", return_value="formatted traceback"
|
|
110
|
+
):
|
|
111
|
+
try:
|
|
112
|
+
raise ValueError("Test exception")
|
|
113
|
+
except ValueError:
|
|
114
|
+
self.response.exception()
|
|
115
|
+
|
|
116
|
+
self.assertEqual(self.response.status(), 500)
|
|
117
|
+
exception_info = self.response.body["python_exception"]
|
|
118
|
+
self.assertEqual(exception_info["value"], "Test exception")
|
|
119
|
+
self.assertEqual(exception_info["traceback"], "formatted traceback")
|
|
120
|
+
|
|
121
|
+
def test_exception_handling_debug_off(self):
|
|
122
|
+
with patch("your_module.DEBUG", False), patch(
|
|
123
|
+
"traceback.format_exc", return_value="formatted traceback"
|
|
124
|
+
):
|
|
125
|
+
try:
|
|
126
|
+
raise ValueError("Test exception")
|
|
127
|
+
except ValueError:
|
|
128
|
+
self.response.exception()
|
|
129
|
+
|
|
130
|
+
self.assertEqual(self.response.status(), 500)
|
|
131
|
+
exception_info = self.response.body["python_exception"]
|
|
132
|
+
self.assertEqual(exception_info["value"], "Test exception")
|
|
133
|
+
self.assertIsNone(exception_info["traceback"])
|
|
134
|
+
|
|
135
|
+
def test_chaining_methods(self):
|
|
136
|
+
response = (
|
|
137
|
+
self.response.set_status(201)
|
|
138
|
+
.alert("Chain Alert")
|
|
139
|
+
.toast("Chain Toast", "info")
|
|
140
|
+
.redirect("https://chained-example.com")
|
|
141
|
+
)
|
|
142
|
+
self.assertEqual(response.status(), 201)
|
|
143
|
+
self.assertEqual(len(response.actions), 3)
|
|
144
|
+
self.assertEqual(
|
|
145
|
+
response.actions[2]["payload"]["location"], "https://chained-example.com"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_render(self):
|
|
149
|
+
self.response.set_body({"key": "value"})
|
|
150
|
+
rendered_response = self.response.render()
|
|
151
|
+
self.assertEqual(rendered_response["statusCode"], 200)
|
|
152
|
+
self.assertEqual(
|
|
153
|
+
rendered_response["headers"]["Content-Type"], "application/json"
|
|
154
|
+
)
|
|
155
|
+
self.assertIn('"key": "value"', rendered_response["body"])
|
|
156
|
+
|
|
157
|
+
def test_format_header_key(self):
|
|
158
|
+
result = Response._format_header_key("x-custom-header")
|
|
159
|
+
self.assertEqual(result, "X-Custom-Header")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
unittest.main()
|
velocity/db/core/decorators.py
CHANGED
|
@@ -99,7 +99,8 @@ def return_default(
|
|
|
99
99
|
|
|
100
100
|
def create_missing(func):
|
|
101
101
|
"""
|
|
102
|
-
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
102
|
+
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
103
|
+
tries to create them and re-run (only if schema is not locked).
|
|
103
104
|
"""
|
|
104
105
|
|
|
105
106
|
@wraps(func)
|
|
@@ -109,8 +110,16 @@ def create_missing(func):
|
|
|
109
110
|
result = func(self, *args, **kwds)
|
|
110
111
|
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
111
112
|
return result
|
|
112
|
-
except exceptions.DbColumnMissingError:
|
|
113
|
+
except exceptions.DbColumnMissingError as e:
|
|
113
114
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
115
|
+
|
|
116
|
+
# Check if schema is locked
|
|
117
|
+
if self.tx.engine.schema_locked:
|
|
118
|
+
raise exceptions.DbSchemaLockedError(
|
|
119
|
+
f"Cannot create missing column: schema is locked. Original error: {e}"
|
|
120
|
+
) from e
|
|
121
|
+
|
|
122
|
+
# Existing logic for automatic creation
|
|
114
123
|
data = {}
|
|
115
124
|
if "pk" in kwds:
|
|
116
125
|
data.update(kwds["pk"])
|
|
@@ -121,8 +130,16 @@ def create_missing(func):
|
|
|
121
130
|
data.update(arg)
|
|
122
131
|
self.alter(data)
|
|
123
132
|
return func(self, *args, **kwds)
|
|
124
|
-
except exceptions.DbTableMissingError:
|
|
133
|
+
except exceptions.DbTableMissingError as e:
|
|
125
134
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
135
|
+
|
|
136
|
+
# Check if schema is locked
|
|
137
|
+
if self.tx.engine.schema_locked:
|
|
138
|
+
raise exceptions.DbSchemaLockedError(
|
|
139
|
+
f"Cannot create missing table: schema is locked. Original error: {e}"
|
|
140
|
+
) from e
|
|
141
|
+
|
|
142
|
+
# Existing logic for automatic creation
|
|
126
143
|
data = {}
|
|
127
144
|
if "pk" in kwds:
|
|
128
145
|
data.update(kwds["pk"])
|
velocity/db/core/engine.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
3
5
|
from functools import wraps
|
|
4
6
|
from velocity.db import exceptions
|
|
5
7
|
from velocity.db.core.transaction import Transaction
|
|
@@ -17,11 +19,12 @@ class Engine:
|
|
|
17
19
|
|
|
18
20
|
MAX_RETRIES = 100
|
|
19
21
|
|
|
20
|
-
def __init__(self, driver, config, sql, connect_timeout=5):
|
|
22
|
+
def __init__(self, driver, config, sql, connect_timeout=5, schema_locked=False):
|
|
21
23
|
self.__config = config
|
|
22
24
|
self.__sql = sql
|
|
23
25
|
self.__driver = driver
|
|
24
26
|
self.__connect_timeout = connect_timeout
|
|
27
|
+
self.__schema_locked = schema_locked
|
|
25
28
|
|
|
26
29
|
def __str__(self):
|
|
27
30
|
return f"[{self.sql.server}] engine({self.config})"
|
|
@@ -205,6 +208,29 @@ class Engine:
|
|
|
205
208
|
def sql(self):
|
|
206
209
|
return self.__sql
|
|
207
210
|
|
|
211
|
+
@property
|
|
212
|
+
def schema_locked(self):
|
|
213
|
+
"""Returns True if schema modifications are locked."""
|
|
214
|
+
return self.__schema_locked
|
|
215
|
+
|
|
216
|
+
def lock_schema(self):
|
|
217
|
+
"""Lock schema to prevent automatic modifications."""
|
|
218
|
+
self.__schema_locked = True
|
|
219
|
+
|
|
220
|
+
def unlock_schema(self):
|
|
221
|
+
"""Unlock schema to allow automatic modifications."""
|
|
222
|
+
self.__schema_locked = False
|
|
223
|
+
|
|
224
|
+
@contextmanager
|
|
225
|
+
def unlocked_schema(self):
|
|
226
|
+
"""Temporarily unlock schema for automatic creation."""
|
|
227
|
+
original_state = self.__schema_locked
|
|
228
|
+
self.__schema_locked = False
|
|
229
|
+
try:
|
|
230
|
+
yield self
|
|
231
|
+
finally:
|
|
232
|
+
self.__schema_locked = original_state
|
|
233
|
+
|
|
208
234
|
@property
|
|
209
235
|
def version(self):
|
|
210
236
|
"""
|
|
@@ -353,12 +379,12 @@ class Engine:
|
|
|
353
379
|
if sql:
|
|
354
380
|
formatted_sql_info = f" sql={self._format_sql_with_params(sql, parameters)}"
|
|
355
381
|
|
|
356
|
-
logger.warning(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
382
|
+
# logger.warning(
|
|
383
|
+
# "Database error caught. Attempting to transform: code=%s message=%s%s",
|
|
384
|
+
# error_code,
|
|
385
|
+
# error_message,
|
|
386
|
+
# formatted_sql_info,
|
|
387
|
+
# )
|
|
362
388
|
|
|
363
389
|
# Direct error code mapping
|
|
364
390
|
if error_code in self.sql.ApplicationErrorCodes:
|
velocity/db/exceptions.py
CHANGED
|
@@ -93,6 +93,12 @@ class DbTransactionError(DbException):
|
|
|
93
93
|
pass
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
class DbSchemaLockedError(DbApplicationError):
|
|
97
|
+
"""Raised when attempting to modify schema while schema is locked."""
|
|
98
|
+
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
96
102
|
class DuplicateRowsFoundError(Exception):
|
|
97
103
|
"""Multiple rows found when expecting single result."""
|
|
98
104
|
|
|
@@ -125,5 +131,6 @@ __all__ = [
|
|
|
125
131
|
"DbDataIntegrityError",
|
|
126
132
|
"DbQueryError",
|
|
127
133
|
"DbTransactionError",
|
|
134
|
+
"DbSchemaLockedError",
|
|
128
135
|
"DuplicateRowsFoundError",
|
|
129
136
|
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base abstract classes for database server implementations.
|
|
3
|
+
"""
|
|
4
|
+
from .sql import BaseSQLDialect
|
|
5
|
+
from .types import BaseTypes
|
|
6
|
+
from .operators import BaseOperators
|
|
7
|
+
from .initializer import BaseInitializer
|
|
8
|
+
|
|
9
|
+
__all__ = ["BaseSQLDialect", "BaseTypes", "BaseOperators", "BaseInitializer"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for database initialization.
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from velocity.db.core import engine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseInitializer(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for database connection initialization.
|
|
12
|
+
|
|
13
|
+
Each database implementation should provide a concrete implementation
|
|
14
|
+
of the initialize method to set up database connections properly.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def initialize(config: Optional[Dict[str, Any]] = None, schema_locked: bool = False, **kwargs) -> engine.Engine:
|
|
20
|
+
"""
|
|
21
|
+
Initialize a database engine with the appropriate driver and configuration.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Configuration dictionary (can be None)
|
|
25
|
+
schema_locked: Boolean to lock schema modifications (default: False)
|
|
26
|
+
**kwargs: Additional configuration parameters
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Configured Engine instance
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ImportError: If required database driver is not available
|
|
33
|
+
ValueError: If configuration is invalid
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _merge_config(base_config: Dict[str, Any], config: Optional[Dict[str, Any]], **kwargs) -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Helper method to merge configuration from multiple sources.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
base_config: Base configuration (e.g., from environment)
|
|
44
|
+
config: User-provided configuration
|
|
45
|
+
**kwargs: Additional keyword arguments
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Merged configuration dictionary
|
|
49
|
+
"""
|
|
50
|
+
final_config = base_config.copy()
|
|
51
|
+
if config:
|
|
52
|
+
final_config.update(config)
|
|
53
|
+
final_config.update(kwargs)
|
|
54
|
+
return final_config
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _validate_required_config(config: Dict[str, Any], required_keys: list[str]) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Validate that required configuration keys are present.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config: Configuration to validate
|
|
63
|
+
required_keys: List of required configuration keys
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If required keys are missing
|
|
67
|
+
"""
|
|
68
|
+
missing_keys = [key for key in required_keys if key not in config]
|
|
69
|
+
if missing_keys:
|
|
70
|
+
raise ValueError(f"Missing required configuration keys: {missing_keys}")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for database operator mapping implementations.
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseOperators(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class that defines the interface for database operator mappings.
|
|
11
|
+
|
|
12
|
+
Each database implementation should provide concrete implementations of operator
|
|
13
|
+
mappings to handle conversion between Velocity.DB operators and SQL operators.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def get_operators(cls) -> Dict[str, str]:
|
|
19
|
+
"""
|
|
20
|
+
Returns a dictionary mapping Velocity.DB operators to SQL operators.
|
|
21
|
+
|
|
22
|
+
This method should return a complete mapping of all operators supported
|
|
23
|
+
by this database implementation.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dictionary mapping operator symbols to SQL operators
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
{
|
|
30
|
+
"=": "=",
|
|
31
|
+
"!=": "<>",
|
|
32
|
+
"<>": "<>",
|
|
33
|
+
"%": "LIKE",
|
|
34
|
+
"!%": "NOT LIKE",
|
|
35
|
+
"%%": "ILIKE", # PostgreSQL case-insensitive
|
|
36
|
+
"!%%": "NOT ILIKE",
|
|
37
|
+
"><": "BETWEEN",
|
|
38
|
+
"!><": "NOT BETWEEN",
|
|
39
|
+
# ... etc
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_base_operators(cls) -> Dict[str, str]:
|
|
46
|
+
"""
|
|
47
|
+
Returns common operators supported by most databases.
|
|
48
|
+
Subclasses can use this as a starting point and override specific operators.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary of common SQL operators
|
|
52
|
+
"""
|
|
53
|
+
return {
|
|
54
|
+
"=": "=",
|
|
55
|
+
"==": "=",
|
|
56
|
+
"!=": "<>",
|
|
57
|
+
"<>": "<>",
|
|
58
|
+
"!": "<>",
|
|
59
|
+
"<": "<",
|
|
60
|
+
">": ">",
|
|
61
|
+
"<=": "<=",
|
|
62
|
+
">=": ">=",
|
|
63
|
+
"%": "LIKE",
|
|
64
|
+
"!%": "NOT LIKE",
|
|
65
|
+
"><": "BETWEEN",
|
|
66
|
+
"!><": "NOT BETWEEN",
|
|
67
|
+
">!<": "NOT BETWEEN",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def supports_case_insensitive_like(cls) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Returns True if this database supports case-insensitive LIKE operations.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if database supports ILIKE or similar
|
|
77
|
+
"""
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def supports_regex(cls) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Returns True if this database supports regular expressions.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if database supports regex operators
|
|
87
|
+
"""
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_regex_operators(cls) -> Dict[str, str]:
|
|
92
|
+
"""
|
|
93
|
+
Returns regex operators if supported by this database.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary of regex operators or empty dict if not supported
|
|
97
|
+
"""
|
|
98
|
+
return {}
|