velocity-python 0.0.35__py3-none-any.whl → 0.0.65__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.
velocity/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.35"
1
+ __version__ = version = "0.0.65"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -2,64 +2,34 @@ from velocity.db import exceptions
2
2
  from velocity.db.core.decorators import return_default
3
3
 
4
4
 
5
- class Column(object):
5
+ class Column:
6
6
  """
7
7
  Represents a column in a database table.
8
8
  """
9
9
 
10
10
  def __init__(self, table, name):
11
- """
12
- Initializes a column object with the specified table and name.
13
-
14
- Args:
15
- table (table): The table object that the column belongs to.
16
- name (str): The name of the column.
17
-
18
- Raises:
19
- Exception: If the table parameter is not of type 'table'.
20
- """
21
11
  if isinstance(table, str):
22
- raise Exception("column table parameter must be a `table` class.")
12
+ raise Exception("Column 'table' parameter must be a Table instance.")
23
13
  self.tx = table.tx
24
14
  self.sql = table.tx.engine.sql
25
15
  self.name = name
26
16
  self.table = table
27
17
 
28
18
  def __str__(self):
29
- """
30
- Returns a string representation of the column object.
31
-
32
- Returns:
33
- str: A string representation of the column object.
34
- """
35
- return """
36
- Table: %s
37
- Column: %s
38
- Column Exists: %s
39
- Py Type: %s
40
- SQL Type: %s
41
- NULL OK: %s
42
- Foreign Key: %s
43
- """ % (
44
- self.table.name,
45
- self.name,
46
- self.exists(),
47
- self.py_type,
48
- self.sql_type,
49
- self.is_nullok,
50
- self.foreign_key_to,
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"
51
27
  )
52
28
 
53
29
  @property
54
30
  def info(self):
55
31
  """
56
- Retrieves information about the column from the database.
57
-
58
- Returns:
59
- dict: A dictionary containing information about the column.
60
-
61
- Raises:
62
- DbColumnMissingError: If the column does not exist in the database.
32
+ Retrieves information about the column from the database, raising DbColumnMissingError if not found.
63
33
  """
64
34
  sql, vals = self.sql.column_info(self.table.name, self.name)
65
35
  result = self.tx.execute(sql, vals).one()
@@ -70,13 +40,7 @@ class Column(object):
70
40
  @property
71
41
  def foreign_key_info(self):
72
42
  """
73
- Retrieves information about the foreign key constraint on the column.
74
-
75
- Returns:
76
- dict: A dictionary containing information about the foreign key constraint.
77
-
78
- Raises:
79
- DbColumnMissingError: If the column does not exist in the database.
43
+ Retrieves information about any foreign key constraint on this column.
80
44
  """
81
45
  sql, vals = self.sql.foreign_key_info(table=self.table.name, column=self.name)
82
46
  result = self.tx.execute(sql, vals).one()
@@ -87,13 +51,7 @@ class Column(object):
87
51
  @property
88
52
  def foreign_key_to(self):
89
53
  """
90
- Retrieves the name of the referenced table and column for the foreign key constraint.
91
-
92
- Returns:
93
- str: The name of the referenced table and column in the format 'referenced_table_name.referenced_column_name'.
94
-
95
- Raises:
96
- DbColumnMissingError: If the column does not exist in the database.
54
+ Returns a string 'referenced_table_name.referenced_column_name' or None if no foreign key.
97
55
  """
98
56
  try:
99
57
  return "{referenced_table_name}.{referenced_column_name}".format(
@@ -105,13 +63,7 @@ class Column(object):
105
63
  @property
106
64
  def foreign_key_table(self):
107
65
  """
108
- Retrieves the name of the referenced table for the foreign key constraint.
109
-
110
- Returns:
111
- str: The name of the referenced table.
112
-
113
- Raises:
114
- DbColumnMissingError: If the column does not exist in the database.
66
+ Returns the name of the referenced table for the foreign key, or None if none.
115
67
  """
116
68
  try:
117
69
  return self.foreign_key_info["referenced_table_name"]
@@ -120,40 +72,28 @@ class Column(object):
120
72
 
121
73
  def exists(self):
122
74
  """
123
- Checks if the column exists in the table.
124
-
125
- Returns:
126
- bool: True if the column exists, False otherwise.
75
+ True if this column name is in self.table.columns().
127
76
  """
128
- return self.name in self.table.columns
77
+ return self.name in self.table.columns()
129
78
 
130
79
  @property
131
80
  def py_type(self):
132
81
  """
133
- Retrieves the Python data type of the column.
134
-
135
- Returns:
136
- type: The Python data type of the column.
82
+ Returns the Python data type that corresponds to this column's SQL type.
137
83
  """
138
- return self.sql.py_type(self.sql_type)
84
+ return self.sql.types.py_type(self.sql_type)
139
85
 
140
86
  @property
141
87
  def sql_type(self):
142
88
  """
143
- Retrieves the SQL data type of the column.
144
-
145
- Returns:
146
- str: The SQL data type of the column.
89
+ Returns the underlying SQL type name (e.g. 'TEXT', 'INTEGER', etc.).
147
90
  """
148
91
  return self.info[self.sql.type_column_identifier]
149
92
 
150
93
  @property
151
94
  def is_nullable(self):
152
95
  """
153
- Checks if the column is nullable.
154
-
155
- Returns:
156
- bool: True if the column is nullable, False otherwise.
96
+ True if column is nullable.
157
97
  """
158
98
  return self.info[self.sql.is_nullable]
159
99
 
@@ -162,9 +102,6 @@ class Column(object):
162
102
  def rename(self, name):
163
103
  """
164
104
  Renames the column.
165
-
166
- Args:
167
- name (str): The new name for the column.
168
105
  """
169
106
  sql, vals = self.sql.rename_column(self.table.name, self.name, name)
170
107
  self.tx.execute(sql, vals)
@@ -173,40 +110,23 @@ class Column(object):
173
110
  @return_default([])
174
111
  def distinct(self, order="asc", qty=None):
175
112
  """
176
- Retrieves distinct values from the column.
177
-
178
- Args:
179
- order (str, optional): The order in which the values should be sorted. Defaults to 'asc'.
180
- qty (int, optional): The maximum number of distinct values to retrieve. Defaults to None.
181
-
182
- Returns:
183
- list: A list of distinct values from the column.
113
+ Returns the distinct values in this column, optionally ordered and/or limited in quantity.
184
114
  """
185
115
  sql, vals = self.sql.select(
186
- columns="distinct {}".format(self.name),
116
+ columns=f"distinct {self.name}",
187
117
  table=self.table.name,
188
- orderby="{} {}".format(self.name, order),
118
+ orderby=f"{self.name} {order}",
189
119
  qty=qty,
190
120
  )
191
121
  return self.tx.execute(sql, vals).as_simple_list().all()
192
122
 
193
123
  def max(self, where=None):
194
124
  """
195
- Retrieves the maximum value from the column.
196
-
197
- Args:
198
- where (str, optional): The WHERE clause to filter the rows. Defaults to None.
199
-
200
- Returns:
201
- int: The maximum value from the column.
202
-
203
- Raises:
204
- DbTableMissingError: If the table does not exist in the database.
205
- DbColumnMissingError: If the column does not exist in the database.
125
+ Returns the MAX() of this column, or 0 if table/column is missing.
206
126
  """
207
127
  try:
208
128
  sql, vals = self.sql.select(
209
- columns="max({})".format(self.name), table=self.table.name, where=where
129
+ columns=f"max({self.name})", table=self.table.name, where=where
210
130
  )
211
131
  return self.tx.execute(sql, vals).scalar()
212
132
  except (exceptions.DbTableMissingError, exceptions.DbColumnMissingError):
@@ -1,4 +1,7 @@
1
- class Database(object):
1
+ class Database:
2
+ """
3
+ Represents a database within a transaction context.
4
+ """
2
5
 
3
6
  def __init__(self, tx, name=None):
4
7
  self.tx = tx
@@ -6,16 +9,11 @@ class Database(object):
6
9
  self.sql = tx.engine.sql
7
10
 
8
11
  def __str__(self):
9
- return """
10
- Engine: %s
11
- Database: %s
12
- (db exists) %s
13
- Tables: %s
14
- """ % (
15
- self.tx.engine.sql.server,
16
- self.name,
17
- self.exists(),
18
- len(self.tables),
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"
19
17
  )
20
18
 
21
19
  def __enter__(self):
@@ -26,40 +24,98 @@ class Database(object):
26
24
  self.close()
27
25
 
28
26
  def close(self):
27
+ """
28
+ Closes the cursor if it exists.
29
+ """
29
30
  try:
30
31
  self._cursor.close()
31
- # print("*** database('{}').cursor.close()".format(self.name))
32
32
  except AttributeError:
33
33
  pass
34
34
 
35
- @property
36
35
  def cursor(self):
36
+ """
37
+ Lazy-initialize the cursor on first use.
38
+ """
37
39
  try:
38
40
  return self._cursor
39
41
  except AttributeError:
40
- # print("*** database('{}').cursor.open()".format(self.name))
41
42
  self._cursor = self.tx.cursor()
42
43
  return self._cursor
43
44
 
44
45
  def drop(self):
45
- sql, vals = self.engine.sql.drop_database(self.name)
46
- self.tx.execute(sql, vals, single=True, cursor=self.cursor)
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())
47
51
 
48
52
  def create(self):
49
- sql, vals = self.engine.sql.create_database(self.name)
50
- self.tx.execute(sql, vals, single=True, cursor=self.cursor)
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())
51
58
 
52
59
  def exists(self):
60
+ """
61
+ Returns True if the database exists, else False.
62
+ """
53
63
  sql, vals = self.sql.databases()
54
- result = self.tx.execute(sql, vals, cursor=self.cursor)
64
+ result = self.tx.execute(sql, vals, cursor=self.cursor())
55
65
  return bool(self.name in [x[0] for x in result.as_tuple()])
56
66
 
57
67
  @property
58
68
  def tables(self):
69
+ """
70
+ Returns a list of 'schema.table' strings representing tables in this database.
71
+ """
59
72
  sql, vals = self.sql.tables()
60
- result = self.tx.execute(sql, vals, cursor=self.cursor)
61
- return ["%s.%s" % x for x in result.as_tuple()]
73
+ result = self.tx.execute(sql, vals, cursor=self.cursor())
74
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
62
75
 
63
76
  def reindex(self):
64
- sql, vals = "REINDEX DATABASE {}".format(self.name), tuple()
65
- self.tx.execute(sql, vals, cursor=self.cursor)
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()
@@ -1,34 +1,61 @@
1
+ import time
2
+ import random
3
+ import traceback
1
4
  from functools import wraps
2
5
  from velocity.db import exceptions
3
- import traceback
4
6
 
5
7
 
6
- def retry_on_dup_key(function):
7
- # retry_on_dup_key is needed to skip past existing autogenerated ids when inserting
8
- @wraps(function)
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)
9
14
  def retry_decorator(self, *args, **kwds):
10
- sp = self.tx.create_savepoint(cursor=self.cursor)
11
- while True:
15
+ max_retries = 10
16
+ retries = 0
17
+ while retries < max_retries:
18
+ sp = self.tx.create_savepoint(cursor=self.cursor())
12
19
  try:
13
- return function(self, *args, **kwds)
20
+ result = func(self, *args, **kwds)
21
+ self.tx.release_savepoint(sp, cursor=self.cursor())
22
+ return result
14
23
  except exceptions.DbDuplicateKeyError:
15
- self.tx.rollback_savepoint(sp, cursor=self.cursor)
16
- continue
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.")
17
33
 
18
34
  return retry_decorator
19
35
 
20
36
 
21
- def reset_id_on_dup_key(function):
22
- # reset_id_on_dup_key sets sys_id to max(sys_id) + 1 when inserting
23
- @wraps(function)
24
- def reset_decorator(self, *args, **kwds):
25
- sp = self.tx.create_savepoint(cursor=self.cursor)
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())
26
45
  try:
27
- return function(self, *args, **kwds)
46
+ result = func(self, *args, **kwds)
47
+ self.tx.release_savepoint(sp, cursor=self.cursor())
48
+ return result
28
49
  except exceptions.DbDuplicateKeyError:
29
- self.tx.rollback_savepoint(sp, cursor=self.cursor)
30
- self.set_sequence(self.max("sys_id") + 1)
31
- return function(self, *args, **kwds)
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.")
32
59
 
33
60
  return reset_decorator
34
61
 
@@ -44,59 +71,69 @@ def return_default(
44
71
  exceptions.DbObjectExistsError,
45
72
  ),
46
73
  ):
47
- def decorator(f):
48
- f.default = default
49
- f.exceptions = exceptions
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
50
82
 
51
- @wraps(f)
52
- def return_default(self, *args, **kwds):
53
- sp = self.tx.create_savepoint(cursor=self.cursor)
83
+ @wraps(func)
84
+ def wrapper(self, *args, **kwds):
85
+ sp = self.tx.create_savepoint(cursor=self.cursor())
54
86
  try:
55
- result = f(self, *args, **kwds)
87
+ result = func(self, *args, **kwds)
56
88
  if result is None:
57
89
  result = default
58
- except f.exceptions as e:
90
+ except func.exceptions:
59
91
  traceback.print_exc()
60
- self.tx.rollback_savepoint(sp, cursor=self.cursor)
61
- return f.default
62
- self.tx.release_savepoint(sp, cursor=self.cursor)
92
+ self.tx.rollback_savepoint(sp, cursor=self.cursor())
93
+ return default
94
+ self.tx.release_savepoint(sp, cursor=self.cursor())
63
95
  return result
64
96
 
65
- return return_default
97
+ return wrapper
66
98
 
67
99
  return decorator
68
100
 
69
101
 
70
- def create_missing(function):
71
- @wraps(function)
72
- def create_missing_decorator(self, *args, **kwds):
73
- sp = self.tx.create_savepoint(cursor=self.cursor)
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())
74
110
  try:
75
- result = function(self, *args, **kwds)
76
- self.tx.release_savepoint(sp, cursor=self.cursor)
111
+ result = func(self, *args, **kwds)
112
+ self.tx.release_savepoint(sp, cursor=self.cursor())
113
+ return result
77
114
  except exceptions.DbColumnMissingError:
78
- self.tx.rollback_savepoint(sp, cursor=self.cursor)
115
+ self.tx.rollback_savepoint(sp, cursor=self.cursor())
79
116
  data = {}
80
117
  if "pk" in kwds:
81
118
  data.update(kwds["pk"])
82
119
  if "data" in kwds:
83
120
  data.update(kwds["data"])
84
- for i in range(len(args)):
85
- if args[i]:
86
- data.update(args[i])
121
+ for i, arg in enumerate(args):
122
+ if isinstance(arg, dict):
123
+ data.update(arg)
87
124
  self.alter(data)
88
- result = function(self, *args, **kwds)
125
+ return func(self, *args, **kwds)
89
126
  except exceptions.DbTableMissingError:
90
- self.tx.rollback_savepoint(sp, cursor=self.cursor)
127
+ self.tx.rollback_savepoint(sp, cursor=self.cursor())
91
128
  data = {}
92
129
  if "pk" in kwds:
93
130
  data.update(kwds["pk"])
94
131
  if "data" in kwds:
95
132
  data.update(kwds["data"])
96
- for i in range(len(args)):
97
- data.update(args[i])
133
+ for i, arg in enumerate(args):
134
+ if isinstance(arg, dict):
135
+ data.update(arg)
98
136
  self.create(data)
99
- result = function(self, *args, **kwds)
100
- return result
137
+ return func(self, *args, **kwds)
101
138
 
102
- return create_missing_decorator
139
+ return wrapper