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.

Files changed (73) hide show
  1. {velocity_python-0.0.35 → velocity_python-0.0.65}/PKG-INFO +3 -2
  2. {velocity_python-0.0.35 → velocity_python-0.0.65}/pyproject.toml +1 -1
  3. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.65/src/velocity/db/core/column.py +133 -0
  5. velocity_python-0.0.65/src/velocity/db/core/database.py +121 -0
  6. velocity_python-0.0.65/src/velocity/db/core/decorators.py +139 -0
  7. velocity_python-0.0.65/src/velocity/db/core/engine.py +373 -0
  8. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/result.py +94 -49
  9. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/row.py +81 -46
  10. velocity_python-0.0.65/src/velocity/db/core/sequence.py +131 -0
  11. velocity_python-0.0.65/src/velocity/db/core/table.py +1045 -0
  12. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/transaction.py +75 -77
  13. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/mysql.py +4 -0
  14. velocity_python-0.0.65/src/velocity/db/servers/postgres/__init__.py +19 -0
  15. velocity_python-0.0.65/src/velocity/db/servers/postgres/operators.py +23 -0
  16. 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
  17. velocity_python-0.0.65/src/velocity/db/servers/postgres/types.py +109 -0
  18. velocity_python-0.0.65/src/velocity/db/servers/tablehelper.py +277 -0
  19. velocity_python-0.0.65/src/velocity/misc/conv/iconv.py +375 -0
  20. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/conv/oconv.py +5 -4
  21. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/db.py +2 -2
  22. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/PKG-INFO +3 -2
  23. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/SOURCES.txt +7 -4
  24. velocity_python-0.0.65/tests/test_iconv.py +203 -0
  25. velocity_python-0.0.65/tests/test_sql_builder.py +165 -0
  26. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_timer.py +3 -3
  27. velocity_python-0.0.35/src/velocity/db/core/column.py +0 -213
  28. velocity_python-0.0.35/src/velocity/db/core/database.py +0 -65
  29. velocity_python-0.0.35/src/velocity/db/core/decorators.py +0 -102
  30. velocity_python-0.0.35/src/velocity/db/core/engine.py +0 -372
  31. velocity_python-0.0.35/src/velocity/db/core/sequence.py +0 -41
  32. velocity_python-0.0.35/src/velocity/db/core/table.py +0 -628
  33. velocity_python-0.0.35/src/velocity/misc/conv/iconv.py +0 -189
  34. velocity_python-0.0.35/tests/test_foreign_key_handling.py +0 -169
  35. velocity_python-0.0.35/tests/test_iconv.py +0 -207
  36. velocity_python-0.0.35/tests/test_postgres_advanced.py +0 -273
  37. {velocity_python-0.0.35 → velocity_python-0.0.65}/LICENSE +0 -0
  38. {velocity_python-0.0.35 → velocity_python-0.0.65}/README.md +0 -0
  39. {velocity_python-0.0.35 → velocity_python-0.0.65}/setup.cfg +0 -0
  40. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/__init__.py +0 -0
  41. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/__init__.py +0 -0
  42. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/context.py +0 -0
  43. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  44. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/response.py +0 -0
  45. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  46. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/__init__.py +0 -0
  47. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/__init__.py +0 -0
  48. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/core/exceptions.py +0 -0
  49. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/__init__.py +0 -0
  50. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/mysql_reserved.py +0 -0
  51. 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
  52. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlite.py +0 -0
  53. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlite_reserved.py +0 -0
  54. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlserver.py +0 -0
  55. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
  56. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/__init__.py +0 -0
  57. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/conv/__init__.py +0 -0
  58. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/export.py +0 -0
  59. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/format.py +2 -2
  60. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/mail.py +0 -0
  61. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/merge.py +0 -0
  62. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity/misc/timer.py +0 -0
  63. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  64. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/requires.txt +0 -0
  65. {velocity_python-0.0.35 → velocity_python-0.0.65}/src/velocity_python.egg-info/top_level.txt +0 -0
  66. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_db.py +0 -0
  67. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_email_processing.py +0 -0
  68. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_format.py +0 -0
  69. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_merge.py +0 -0
  70. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_oconv.py +0 -0
  71. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_postgres.py +0 -0
  72. {velocity_python-0.0.35 → velocity_python-0.0.65}/tests/test_response.py +0 -0
  73. {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
1
+ Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.35
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "velocity-python"
3
- version = "0.0.35"
3
+ version = "0.0.65"
4
4
  authors = [
5
5
  { name="Paul Perez", email="pperez@codeclubs.org" },
6
6
  ]
@@ -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
@@ -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