velocity-python 0.0.35__tar.gz → 0.0.65__tar.gz
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_python-0.0.35 → velocity_python-0.0.65}/PKG-INFO +3 -2
- {velocity_python-0.0.35 → velocity_python-0.0.65}/pyproject.toml +1 -1
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/__init__.py +1 -1
- velocity_python-0.0.65/src/velocity/db/core/column.py +133 -0
- velocity_python-0.0.65/src/velocity/db/core/database.py +121 -0
- velocity_python-0.0.65/src/velocity/db/core/decorators.py +139 -0
- velocity_python-0.0.65/src/velocity/db/core/engine.py +373 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/result.py +94 -49
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/row.py +81 -46
- velocity_python-0.0.65/src/velocity/db/core/sequence.py +131 -0
- velocity_python-0.0.65/src/velocity/db/core/table.py +1045 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/transaction.py +75 -77
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/mysql.py +4 -0
- velocity_python-0.0.65/src/velocity/db/servers/postgres/__init__.py +19 -0
- velocity_python-0.0.65/src/velocity/db/servers/postgres/operators.py +23 -0
- velocity_python-0.0.35/src/velocity/db/servers/postgres.py → velocity_python-0.0.65/src/velocity/db/servers/postgres/sql.py +508 -589
- velocity_python-0.0.65/src/velocity/db/servers/postgres/types.py +109 -0
- velocity_python-0.0.65/src/velocity/db/servers/tablehelper.py +277 -0
- velocity_python-0.0.65/src/velocity/misc/conv/iconv.py +375 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/conv/oconv.py +5 -4
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/db.py +2 -2
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/PKG-INFO +3 -2
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/SOURCES.txt +7 -4
- velocity_python-0.0.65/tests/test_iconv.py +203 -0
- velocity_python-0.0.65/tests/test_sql_builder.py +165 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_timer.py +3 -3
- velocity_python-0.0.35/src/velocity/db/core/column.py +0 -213
- velocity_python-0.0.35/src/velocity/db/core/database.py +0 -65
- velocity_python-0.0.35/src/velocity/db/core/decorators.py +0 -102
- velocity_python-0.0.35/src/velocity/db/core/engine.py +0 -372
- velocity_python-0.0.35/src/velocity/db/core/sequence.py +0 -41
- velocity_python-0.0.35/src/velocity/db/core/table.py +0 -628
- velocity_python-0.0.35/src/velocity/misc/conv/iconv.py +0 -189
- velocity_python-0.0.35/tests/test_foreign_key_handling.py +0 -169
- velocity_python-0.0.35/tests/test_iconv.py +0 -207
- velocity_python-0.0.35/tests/test_postgres_advanced.py +0 -273
- {velocity_python-0.0.35 → velocity_python-0.0.65}/LICENSE +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/README.md +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/setup.cfg +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/exceptions.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/mysql_reserved.py +0 -0
- velocity_python-0.0.35/src/velocity/db/servers/postgres_reserved.py → velocity_python-0.0.65/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlite.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlite_reserved.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlserver.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/format.py +2 -2
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_db.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_format.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_merge.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_oconv.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_postgres.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_response.py +0 -0
- {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_spreadsheet_functions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.65
|
|
4
4
|
Summary: A rapid application development library for interfacing with data storage
|
|
5
5
|
Author-email: Paul Perez <pperez@codeclubs.org>
|
|
6
6
|
Project-URL: Homepage, https://codeclubs.org/projects/velocity
|
|
@@ -20,6 +20,7 @@ Provides-Extra: sqlserver
|
|
|
20
20
|
Requires-Dist: python-tds; extra == "sqlserver"
|
|
21
21
|
Provides-Extra: postgres
|
|
22
22
|
Requires-Dist: psycopg2-binary; extra == "postgres"
|
|
23
|
+
Dynamic: license-file
|
|
23
24
|
|
|
24
25
|
# Velocity.DB
|
|
25
26
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from velocity.db import exceptions
|
|
2
|
+
from velocity.db.core.decorators import return_default
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Column:
|
|
6
|
+
"""
|
|
7
|
+
Represents a column in a database table.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, table, name):
|
|
11
|
+
if isinstance(table, str):
|
|
12
|
+
raise Exception("Column 'table' parameter must be a Table instance.")
|
|
13
|
+
self.tx = table.tx
|
|
14
|
+
self.sql = table.tx.engine.sql
|
|
15
|
+
self.name = name
|
|
16
|
+
self.table = table
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return (
|
|
20
|
+
f"Table: {self.table.name}\n"
|
|
21
|
+
f"Column: {self.name}\n"
|
|
22
|
+
f"Column Exists: {self.exists()}\n"
|
|
23
|
+
f"Py Type: {self.py_type}\n"
|
|
24
|
+
f"SQL Type: {self.sql_type}\n"
|
|
25
|
+
f"NULL OK: {self.is_nullok}\n"
|
|
26
|
+
f"Foreign Key: {self.foreign_key_to}\n"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def info(self):
|
|
31
|
+
"""
|
|
32
|
+
Retrieves information about the column from the database, raising DbColumnMissingError if not found.
|
|
33
|
+
"""
|
|
34
|
+
sql, vals = self.sql.column_info(self.table.name, self.name)
|
|
35
|
+
result = self.tx.execute(sql, vals).one()
|
|
36
|
+
if not result:
|
|
37
|
+
raise exceptions.DbColumnMissingError
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def foreign_key_info(self):
|
|
42
|
+
"""
|
|
43
|
+
Retrieves information about any foreign key constraint on this column.
|
|
44
|
+
"""
|
|
45
|
+
sql, vals = self.sql.foreign_key_info(table=self.table.name, column=self.name)
|
|
46
|
+
result = self.tx.execute(sql, vals).one()
|
|
47
|
+
if not result:
|
|
48
|
+
raise exceptions.DbColumnMissingError
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def foreign_key_to(self):
|
|
53
|
+
"""
|
|
54
|
+
Returns a string 'referenced_table_name.referenced_column_name' or None if no foreign key.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return "{referenced_table_name}.{referenced_column_name}".format(
|
|
58
|
+
**self.foreign_key_info
|
|
59
|
+
)
|
|
60
|
+
except exceptions.DbColumnMissingError:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def foreign_key_table(self):
|
|
65
|
+
"""
|
|
66
|
+
Returns the name of the referenced table for the foreign key, or None if none.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
return self.foreign_key_info["referenced_table_name"]
|
|
70
|
+
except exceptions.DbColumnMissingError:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def exists(self):
|
|
74
|
+
"""
|
|
75
|
+
True if this column name is in self.table.columns().
|
|
76
|
+
"""
|
|
77
|
+
return self.name in self.table.columns()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def py_type(self):
|
|
81
|
+
"""
|
|
82
|
+
Returns the Python data type that corresponds to this column's SQL type.
|
|
83
|
+
"""
|
|
84
|
+
return self.sql.types.py_type(self.sql_type)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def sql_type(self):
|
|
88
|
+
"""
|
|
89
|
+
Returns the underlying SQL type name (e.g. 'TEXT', 'INTEGER', etc.).
|
|
90
|
+
"""
|
|
91
|
+
return self.info[self.sql.type_column_identifier]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_nullable(self):
|
|
95
|
+
"""
|
|
96
|
+
True if column is nullable.
|
|
97
|
+
"""
|
|
98
|
+
return self.info[self.sql.is_nullable]
|
|
99
|
+
|
|
100
|
+
is_nullok = is_nullable
|
|
101
|
+
|
|
102
|
+
def rename(self, name):
|
|
103
|
+
"""
|
|
104
|
+
Renames the column.
|
|
105
|
+
"""
|
|
106
|
+
sql, vals = self.sql.rename_column(self.table.name, self.name, name)
|
|
107
|
+
self.tx.execute(sql, vals)
|
|
108
|
+
self.name = name
|
|
109
|
+
|
|
110
|
+
@return_default([])
|
|
111
|
+
def distinct(self, order="asc", qty=None):
|
|
112
|
+
"""
|
|
113
|
+
Returns the distinct values in this column, optionally ordered and/or limited in quantity.
|
|
114
|
+
"""
|
|
115
|
+
sql, vals = self.sql.select(
|
|
116
|
+
columns=f"distinct {self.name}",
|
|
117
|
+
table=self.table.name,
|
|
118
|
+
orderby=f"{self.name} {order}",
|
|
119
|
+
qty=qty,
|
|
120
|
+
)
|
|
121
|
+
return self.tx.execute(sql, vals).as_simple_list().all()
|
|
122
|
+
|
|
123
|
+
def max(self, where=None):
|
|
124
|
+
"""
|
|
125
|
+
Returns the MAX() of this column, or 0 if table/column is missing.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
sql, vals = self.sql.select(
|
|
129
|
+
columns=f"max({self.name})", table=self.table.name, where=where
|
|
130
|
+
)
|
|
131
|
+
return self.tx.execute(sql, vals).scalar()
|
|
132
|
+
except (exceptions.DbTableMissingError, exceptions.DbColumnMissingError):
|
|
133
|
+
return 0
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
class Database:
|
|
2
|
+
"""
|
|
3
|
+
Represents a database within a transaction context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, tx, name=None):
|
|
7
|
+
self.tx = tx
|
|
8
|
+
self.name = name or self.tx.engine.config["database"]
|
|
9
|
+
self.sql = tx.engine.sql
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
return (
|
|
13
|
+
f"Engine: {self.tx.engine.sql.server}\n"
|
|
14
|
+
f"Database: {self.name}\n"
|
|
15
|
+
f"(db exists) {self.exists()}\n"
|
|
16
|
+
f"Tables: {len(self.tables)}\n"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def __enter__(self):
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
23
|
+
if not exc_type:
|
|
24
|
+
self.close()
|
|
25
|
+
|
|
26
|
+
def close(self):
|
|
27
|
+
"""
|
|
28
|
+
Closes the cursor if it exists.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
self._cursor.close()
|
|
32
|
+
except AttributeError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def cursor(self):
|
|
36
|
+
"""
|
|
37
|
+
Lazy-initialize the cursor on first use.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
return self._cursor
|
|
41
|
+
except AttributeError:
|
|
42
|
+
self._cursor = self.tx.cursor()
|
|
43
|
+
return self._cursor
|
|
44
|
+
|
|
45
|
+
def drop(self):
|
|
46
|
+
"""
|
|
47
|
+
Drops this database.
|
|
48
|
+
"""
|
|
49
|
+
sql, vals = self.tx.engine.sql.drop_database(self.name)
|
|
50
|
+
self.tx.execute(sql, vals, single=True, cursor=self.cursor())
|
|
51
|
+
|
|
52
|
+
def create(self):
|
|
53
|
+
"""
|
|
54
|
+
Creates this database.
|
|
55
|
+
"""
|
|
56
|
+
sql, vals = self.tx.engine.sql.create_database(self.name)
|
|
57
|
+
self.tx.execute(sql, vals, single=True, cursor=self.cursor())
|
|
58
|
+
|
|
59
|
+
def exists(self):
|
|
60
|
+
"""
|
|
61
|
+
Returns True if the database exists, else False.
|
|
62
|
+
"""
|
|
63
|
+
sql, vals = self.sql.databases()
|
|
64
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
65
|
+
return bool(self.name in [x[0] for x in result.as_tuple()])
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def tables(self):
|
|
69
|
+
"""
|
|
70
|
+
Returns a list of 'schema.table' strings representing tables in this database.
|
|
71
|
+
"""
|
|
72
|
+
sql, vals = self.sql.tables()
|
|
73
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
74
|
+
return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
|
|
75
|
+
|
|
76
|
+
def reindex(self):
|
|
77
|
+
"""
|
|
78
|
+
Re-indexes this database.
|
|
79
|
+
"""
|
|
80
|
+
sql = f"REINDEX DATABASE {self.name}"
|
|
81
|
+
vals = ()
|
|
82
|
+
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
83
|
+
|
|
84
|
+
def switch(self):
|
|
85
|
+
"""
|
|
86
|
+
Switches the parent transaction to this database.
|
|
87
|
+
"""
|
|
88
|
+
self.tx.switch_to_database(self.name)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def vacuum(self, analyze=True, full=False, reindex=True):
|
|
92
|
+
"""
|
|
93
|
+
Performs VACUUM on this database, optionally FULL, optionally ANALYZE,
|
|
94
|
+
optionally REINDEX.
|
|
95
|
+
"""
|
|
96
|
+
# Manually open a separate connection to run VACUUM in isolation_level=0
|
|
97
|
+
conn = self.tx.engine.connect()
|
|
98
|
+
old_isolation_level = conn.isolation_level
|
|
99
|
+
try:
|
|
100
|
+
# Postgres requires VACUUM to run outside a normal transaction block
|
|
101
|
+
conn.set_isolation_level(0)
|
|
102
|
+
|
|
103
|
+
# Build up the VACUUM command
|
|
104
|
+
parts = ["VACUUM"]
|
|
105
|
+
if full:
|
|
106
|
+
parts.append("FULL")
|
|
107
|
+
if analyze:
|
|
108
|
+
parts.append("ANALYZE")
|
|
109
|
+
|
|
110
|
+
# Execute VACUUM
|
|
111
|
+
with conn.cursor() as cur:
|
|
112
|
+
cur.execute(" ".join(parts))
|
|
113
|
+
|
|
114
|
+
# Optionally REINDEX the database
|
|
115
|
+
if reindex:
|
|
116
|
+
cur.execute(f"REINDEX DATABASE {self.name}")
|
|
117
|
+
|
|
118
|
+
finally:
|
|
119
|
+
# Restore isolation level and close the connection
|
|
120
|
+
conn.set_isolation_level(old_isolation_level)
|
|
121
|
+
conn.close()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import traceback
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from velocity.db import exceptions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def retry_on_dup_key(func):
|
|
9
|
+
"""
|
|
10
|
+
Retries a function call if it raises DbDuplicateKeyError, up to max_retries.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@wraps(func)
|
|
14
|
+
def retry_decorator(self, *args, **kwds):
|
|
15
|
+
max_retries = 10
|
|
16
|
+
retries = 0
|
|
17
|
+
while retries < max_retries:
|
|
18
|
+
sp = self.tx.create_savepoint(cursor=self.cursor())
|
|
19
|
+
try:
|
|
20
|
+
result = func(self, *args, **kwds)
|
|
21
|
+
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
22
|
+
return result
|
|
23
|
+
except exceptions.DbDuplicateKeyError:
|
|
24
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
25
|
+
if "sys_id" in kwds.get("data", {}):
|
|
26
|
+
raise
|
|
27
|
+
retries += 1
|
|
28
|
+
if retries >= max_retries:
|
|
29
|
+
raise
|
|
30
|
+
backoff_time = (2**retries) * 0.01 + random.uniform(0, 0.02)
|
|
31
|
+
time.sleep(backoff_time)
|
|
32
|
+
raise exceptions.DbDuplicateKeyError("Max retries reached.")
|
|
33
|
+
|
|
34
|
+
return retry_decorator
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reset_id_on_dup_key(func):
|
|
38
|
+
"""
|
|
39
|
+
Wraps an INSERT/UPSERT to reset the sys_id sequence on duplicate key collisions.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@wraps(func)
|
|
43
|
+
def reset_decorator(self, *args, retries=0, **kwds):
|
|
44
|
+
sp = self.tx.create_savepoint(cursor=self.cursor())
|
|
45
|
+
try:
|
|
46
|
+
result = func(self, *args, **kwds)
|
|
47
|
+
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
48
|
+
return result
|
|
49
|
+
except exceptions.DbDuplicateKeyError:
|
|
50
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
51
|
+
if "sys_id" in kwds.get("data", {}):
|
|
52
|
+
raise
|
|
53
|
+
if retries < 3:
|
|
54
|
+
backoff_time = (2**retries) * 0.01 + random.uniform(0, 0.02)
|
|
55
|
+
time.sleep(backoff_time)
|
|
56
|
+
self.set_sequence(self.max("sys_id") + 1)
|
|
57
|
+
return reset_decorator(self, *args, retries=retries + 1, **kwds)
|
|
58
|
+
raise exceptions.DbDuplicateKeyError("Max retries reached.")
|
|
59
|
+
|
|
60
|
+
return reset_decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def return_default(
|
|
64
|
+
default=None,
|
|
65
|
+
exceptions=(
|
|
66
|
+
StopIteration,
|
|
67
|
+
exceptions.DbApplicationError,
|
|
68
|
+
exceptions.DbTableMissingError,
|
|
69
|
+
exceptions.DbColumnMissingError,
|
|
70
|
+
exceptions.DbTruncationError,
|
|
71
|
+
exceptions.DbObjectExistsError,
|
|
72
|
+
),
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
If the wrapped function raises one of the specified exceptions, or returns None,
|
|
76
|
+
this decorator returns the `default` value instead.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def decorator(func):
|
|
80
|
+
func.default = default
|
|
81
|
+
func.exceptions = exceptions
|
|
82
|
+
|
|
83
|
+
@wraps(func)
|
|
84
|
+
def wrapper(self, *args, **kwds):
|
|
85
|
+
sp = self.tx.create_savepoint(cursor=self.cursor())
|
|
86
|
+
try:
|
|
87
|
+
result = func(self, *args, **kwds)
|
|
88
|
+
if result is None:
|
|
89
|
+
result = default
|
|
90
|
+
except func.exceptions:
|
|
91
|
+
traceback.print_exc()
|
|
92
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
93
|
+
return default
|
|
94
|
+
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
return wrapper
|
|
98
|
+
|
|
99
|
+
return decorator
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_missing(func):
|
|
103
|
+
"""
|
|
104
|
+
If the function call fails with DbColumnMissingError or DbTableMissingError, tries to create them and re-run.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@wraps(func)
|
|
108
|
+
def wrapper(self, *args, **kwds):
|
|
109
|
+
sp = self.tx.create_savepoint(cursor=self.cursor())
|
|
110
|
+
try:
|
|
111
|
+
result = func(self, *args, **kwds)
|
|
112
|
+
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
113
|
+
return result
|
|
114
|
+
except exceptions.DbColumnMissingError:
|
|
115
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
116
|
+
data = {}
|
|
117
|
+
if "pk" in kwds:
|
|
118
|
+
data.update(kwds["pk"])
|
|
119
|
+
if "data" in kwds:
|
|
120
|
+
data.update(kwds["data"])
|
|
121
|
+
for i, arg in enumerate(args):
|
|
122
|
+
if isinstance(arg, dict):
|
|
123
|
+
data.update(arg)
|
|
124
|
+
self.alter(data)
|
|
125
|
+
return func(self, *args, **kwds)
|
|
126
|
+
except exceptions.DbTableMissingError:
|
|
127
|
+
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
128
|
+
data = {}
|
|
129
|
+
if "pk" in kwds:
|
|
130
|
+
data.update(kwds["pk"])
|
|
131
|
+
if "data" in kwds:
|
|
132
|
+
data.update(kwds["data"])
|
|
133
|
+
for i, arg in enumerate(args):
|
|
134
|
+
if isinstance(arg, dict):
|
|
135
|
+
data.update(arg)
|
|
136
|
+
self.create(data)
|
|
137
|
+
return func(self, *args, **kwds)
|
|
138
|
+
|
|
139
|
+
return wrapper
|