velocity-python 0.0.104__tar.gz → 0.0.106__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.
Files changed (83) hide show
  1. {velocity_python-0.0.104/src/velocity_python.egg-info → velocity_python-0.0.106}/PKG-INFO +55 -4
  2. {velocity_python-0.0.104 → velocity_python-0.0.106}/README.md +52 -0
  3. {velocity_python-0.0.104 → velocity_python-0.0.106}/pyproject.toml +3 -4
  4. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/amplify.py +1 -1
  5. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/__init__.py +11 -0
  6. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/engine.py +7 -7
  7. velocity_python-0.0.106/src/velocity/db/core/result.py +375 -0
  8. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/row.py +15 -3
  9. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/table.py +6 -5
  10. velocity_python-0.0.106/src/velocity/db/utils.py +209 -0
  11. {velocity_python-0.0.104 → velocity_python-0.0.106/src/velocity_python.egg-info}/PKG-INFO +55 -4
  12. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity_python.egg-info/SOURCES.txt +7 -0
  13. velocity_python-0.0.106/tests/test_cursor_rowcount_fix.py +150 -0
  14. velocity_python-0.0.106/tests/test_db_utils.py +195 -0
  15. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_oconv.py +13 -11
  16. velocity_python-0.0.106/tests/test_payment_profile_sorting.py +144 -0
  17. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_postgres.py +47 -42
  18. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_response.py +7 -0
  19. velocity_python-0.0.106/tests/test_result_caching.py +277 -0
  20. velocity_python-0.0.106/tests/test_result_sql_aware.py +115 -0
  21. velocity_python-0.0.106/tests/test_row_get_missing_column.py +68 -0
  22. velocity_python-0.0.104/src/velocity/db/core/result.py +0 -217
  23. {velocity_python-0.0.104 → velocity_python-0.0.106}/LICENSE +0 -0
  24. {velocity_python-0.0.104 → velocity_python-0.0.106}/setup.cfg +0 -0
  25. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/__init__.py +0 -0
  26. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/app/__init__.py +0 -0
  27. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/app/invoices.py +0 -0
  28. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/app/orders.py +0 -0
  29. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/app/payments.py +0 -0
  30. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/app/purchase_orders.py +0 -0
  31. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/__init__.py +0 -0
  32. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/handlers/__init__.py +0 -0
  33. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/handlers/context.py +0 -0
  34. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  35. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/handlers/response.py +0 -0
  36. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  37. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/__init__.py +0 -0
  38. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/column.py +0 -0
  39. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/database.py +0 -0
  40. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/decorators.py +0 -0
  41. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/exceptions.py +0 -0
  42. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/sequence.py +0 -0
  43. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/core/transaction.py +0 -0
  44. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/exceptions.py +0 -0
  45. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/__init__.py +0 -0
  46. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/mysql.py +0 -0
  47. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/mysql_reserved.py +0 -0
  48. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/postgres/__init__.py +0 -0
  49. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/postgres/operators.py +0 -0
  50. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/postgres/reserved.py +0 -0
  51. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/postgres/sql.py +0 -0
  52. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/postgres/types.py +0 -0
  53. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/sqlite.py +0 -0
  54. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/sqlite_reserved.py +0 -0
  55. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/sqlserver.py +0 -0
  56. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
  57. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/db/servers/tablehelper.py +0 -0
  58. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/__init__.py +0 -0
  59. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/conv/__init__.py +0 -0
  60. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/conv/iconv.py +0 -0
  61. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/conv/oconv.py +0 -0
  62. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/db.py +0 -0
  63. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/export.py +0 -0
  64. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/format.py +0 -0
  65. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/mail.py +0 -0
  66. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/merge.py +0 -0
  67. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/timer.py +0 -0
  68. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity/misc/tools.py +0 -0
  69. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  70. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity_python.egg-info/requires.txt +0 -0
  71. {velocity_python-0.0.104 → velocity_python-0.0.106}/src/velocity_python.egg-info/top_level.txt +0 -0
  72. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_db.py +0 -0
  73. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_email_processing.py +0 -0
  74. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_fix.py +0 -0
  75. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_format.py +0 -0
  76. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_iconv.py +0 -0
  77. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_merge.py +0 -0
  78. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_original_error.py +0 -0
  79. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_process_error_robustness.py +0 -0
  80. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_spreadsheet_functions.py +0 -0
  81. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_sql_builder.py +0 -0
  82. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_tablehelper.py +0 -0
  83. {velocity_python-0.0.104 → velocity_python-0.0.106}/tests/test_timer.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.104
3
+ Version: 0.0.106
4
4
  Summary: A rapid application development library for interfacing with data storage
5
- Author-email: Velocity Team <contact@example.com>
6
- License: MIT
5
+ Author-email: Velocity Team <info@codeclubs.org>
6
+ License-Expression: MIT
7
7
  Project-URL: Homepage, https://codeclubs.org/projects/velocity
8
8
  Keywords: database,orm,sql,rapid-development,data-storage
9
9
  Classifier: Development Status :: 3 - Alpha
@@ -16,7 +16,6 @@ Classifier: Programming Language :: Python :: 3.8
16
16
  Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
- Classifier: License :: OSI Approved :: MIT License
20
19
  Classifier: Operating System :: OS Independent
21
20
  Requires-Python: >=3.7
22
21
  Description-Content-Type: text/markdown
@@ -146,6 +145,58 @@ pip install velocity-python[sqlserver]
146
145
  pip install velocity-python[all]
147
146
  ```
148
147
 
148
+ ## Project Structure
149
+
150
+ ```
151
+ velocity-python/
152
+ ├── src/velocity/ # Main package source code
153
+ ├── tests/ # Test suite
154
+ ├── scripts/ # Utility scripts and demos
155
+ │ ├── run_tests.py # Test runner script
156
+ │ ├── bump.py # Version management
157
+ │ ├── demo_*.py # Demo scripts
158
+ │ └── README.md # Script documentation
159
+ ├── docs/ # Documentation
160
+ │ ├── TESTING.md # Testing guide
161
+ │ ├── DUAL_FORMAT_DOCUMENTATION.md
162
+ │ ├── ERROR_HANDLING_IMPROVEMENTS.md
163
+ │ └── sample_error_email.html
164
+ ├── Makefile # Development commands
165
+ ├── pyproject.toml # Package configuration
166
+ └── README.md # This file
167
+ ```
168
+
169
+ ## Development
170
+
171
+ ### Running Tests
172
+
173
+ ```bash
174
+ # Run unit tests (fast, no database required)
175
+ make test-unit
176
+
177
+ # Run integration tests (requires database)
178
+ make test-integration
179
+
180
+ # Run with coverage
181
+ make coverage
182
+
183
+ # Clean cache files
184
+ make clean
185
+ ```
186
+
187
+ ### Using Scripts
188
+
189
+ ```bash
190
+ # Run the test runner directly
191
+ python scripts/run_tests.py --unit --verbose
192
+
193
+ # Version management
194
+ python scripts/bump.py
195
+
196
+ # See all available demo scripts
197
+ ls scripts/demo_*.py
198
+ ```
199
+
149
200
  ## Quick Start
150
201
 
151
202
  ### Database Connection
@@ -95,6 +95,58 @@ pip install velocity-python[sqlserver]
95
95
  pip install velocity-python[all]
96
96
  ```
97
97
 
98
+ ## Project Structure
99
+
100
+ ```
101
+ velocity-python/
102
+ ├── src/velocity/ # Main package source code
103
+ ├── tests/ # Test suite
104
+ ├── scripts/ # Utility scripts and demos
105
+ │ ├── run_tests.py # Test runner script
106
+ │ ├── bump.py # Version management
107
+ │ ├── demo_*.py # Demo scripts
108
+ │ └── README.md # Script documentation
109
+ ├── docs/ # Documentation
110
+ │ ├── TESTING.md # Testing guide
111
+ │ ├── DUAL_FORMAT_DOCUMENTATION.md
112
+ │ ├── ERROR_HANDLING_IMPROVEMENTS.md
113
+ │ └── sample_error_email.html
114
+ ├── Makefile # Development commands
115
+ ├── pyproject.toml # Package configuration
116
+ └── README.md # This file
117
+ ```
118
+
119
+ ## Development
120
+
121
+ ### Running Tests
122
+
123
+ ```bash
124
+ # Run unit tests (fast, no database required)
125
+ make test-unit
126
+
127
+ # Run integration tests (requires database)
128
+ make test-integration
129
+
130
+ # Run with coverage
131
+ make coverage
132
+
133
+ # Clean cache files
134
+ make clean
135
+ ```
136
+
137
+ ### Using Scripts
138
+
139
+ ```bash
140
+ # Run the test runner directly
141
+ python scripts/run_tests.py --unit --verbose
142
+
143
+ # Version management
144
+ python scripts/bump.py
145
+
146
+ # See all available demo scripts
147
+ ls scripts/demo_*.py
148
+ ```
149
+
98
150
  ## Quick Start
99
151
 
100
152
  ### Database Connection
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.104"
7
+ version = "0.0.106"
8
8
  authors = [
9
- { name="Velocity Team", email="contact@example.com" },
9
+ { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
11
11
  description = "A rapid application development library for interfacing with data storage"
12
12
  readme = "README.md"
13
- license = {text = "MIT"}
13
+ license = "MIT"
14
14
  requires-python = ">=3.7"
15
15
  keywords = ["database", "orm", "sql", "rapid-development", "data-storage"]
16
16
  classifiers = [
@@ -24,7 +24,6 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.9",
25
25
  "Programming Language :: Python :: 3.10",
26
26
  "Programming Language :: Python :: 3.11",
27
- "License :: OSI Approved :: MIT License",
28
27
  "Operating System :: OS Independent",
29
28
  ]
30
29
  dependencies = [
@@ -250,7 +250,7 @@ class AmplifyProject:
250
250
  customRules=[
251
251
  {"source": "/<*>", "target": "/index.html", "status": "404-200"},
252
252
  {
253
- "source": f'</^[^.]+$|\.(?!({"|".join(srv_list)})$)([^.]+$)/>',
253
+ "source": f'</^[^.]+$|\\.(?!({"|".join(srv_list)})$)([^.]+$)/>',
254
254
  "target": "/",
255
255
  "status": "200",
256
256
  },
@@ -3,6 +3,17 @@ from velocity.db.servers import postgres
3
3
  from velocity.db.servers import mysql
4
4
  from velocity.db.servers import sqlite
5
5
  from velocity.db.servers import sqlserver
6
+ from velocity.db import utils
6
7
 
7
8
  # Export exceptions at the package level for backward compatibility
8
9
  from velocity.db.exceptions import *
10
+
11
+ # Export commonly used utility functions
12
+ from velocity.db.utils import (
13
+ safe_sort_rows,
14
+ safe_sort_key_none_last,
15
+ safe_sort_key_none_first,
16
+ safe_sort_key_with_default,
17
+ group_by_fields,
18
+ safe_sort_grouped_rows
19
+ )
@@ -335,8 +335,8 @@ class Engine:
335
335
  sql: The SQL statement that caused the error (optional)
336
336
  parameters: The parameters passed to the SQL statement (optional)
337
337
 
338
- Returns:
339
- The appropriate velocity exception to raise
338
+ Raises:
339
+ The appropriate velocity exception with proper chaining
340
340
  """
341
341
  logger = logging.getLogger(__name__)
342
342
 
@@ -369,7 +369,7 @@ class Engine:
369
369
  if error_code in codes:
370
370
  logger.info(f"✅ Successfully classified error: {error_code} → {error_class}")
371
371
  try:
372
- return self._create_exception_with_chaining(
372
+ raise self._create_exception_with_chaining(
373
373
  error_class, error_message, exception, sql, parameters
374
374
  )
375
375
  except Exception as creation_error:
@@ -445,7 +445,7 @@ class Engine:
445
445
  try:
446
446
  if re.search(pattern, error_message_lower):
447
447
  logger.info(f"✅ Classified error by pattern match: '{pattern}' → {error_class}")
448
- return self._create_exception_with_chaining(
448
+ raise self._create_exception_with_chaining(
449
449
  error_class, error_message, exception, sql, parameters
450
450
  )
451
451
  except re.error as regex_error:
@@ -464,7 +464,7 @@ class Engine:
464
464
  f" → Available Classifications: {available_codes or 'None configured'}"
465
465
  )
466
466
 
467
- return self._create_exception_with_chaining(
467
+ raise self._create_exception_with_chaining(
468
468
  'DatabaseError', error_message, exception, sql, parameters
469
469
  )
470
470
 
@@ -1082,7 +1082,7 @@ class Engine:
1082
1082
  else:
1083
1083
  new_exception = ExceptionClass(formatted_message)
1084
1084
 
1085
- # Only set __cause__ if original_exception is actually a BaseException
1085
+ # Only set __cause__ if original_exception is not None and derives from BaseException
1086
1086
  if isinstance(original_exception, BaseException):
1087
1087
  new_exception.__cause__ = original_exception # Preserve exception chain
1088
1088
 
@@ -1105,7 +1105,7 @@ class Engine:
1105
1105
  )
1106
1106
 
1107
1107
  fallback_exception = DatabaseError(formatted_message)
1108
- # Only set __cause__ if original_exception is actually a BaseException
1108
+ # Only set __cause__ if original_exception is not None and derives from BaseException
1109
1109
  if isinstance(original_exception, BaseException):
1110
1110
  fallback_exception.__cause__ = original_exception
1111
1111
  return fallback_exception
@@ -0,0 +1,375 @@
1
+ import datetime
2
+ import decimal
3
+ from velocity.misc.format import to_json
4
+
5
+
6
+ class Result:
7
+ """
8
+ Wraps a database cursor to provide various convenience transformations
9
+ (dict, list, tuple, etc.) and helps iterate over query results.
10
+
11
+ Features:
12
+ - Pre-fetches first row for immediate boolean evaluation
13
+ - Boolean state changes as rows are consumed: bool(result) tells you if MORE rows are available
14
+ - Supports __bool__, is_empty(), has_results() for checking remaining results
15
+ - Efficient iteration without unnecessary fetchall() calls
16
+ - Caches next row to maintain accurate state without redundant database calls
17
+
18
+ Boolean Behavior:
19
+ - Initially: bool(result) = True if query returned any rows
20
+ - After each row: bool(result) = True if more rows are available to fetch
21
+ - After last row: bool(result) = False (no more rows)
22
+ - After one() or scalar(): bool(result) = False (marked as exhausted)
23
+ """
24
+
25
+ def __init__(self, cursor=None, tx=None, sql=None, params=None):
26
+ self._cursor = cursor
27
+ # Safely extract headers from cursor description
28
+ try:
29
+ description = getattr(cursor, "description", []) or []
30
+ self._headers = []
31
+ for col in description:
32
+ if hasattr(col, '__getitem__'): # Tuple-like (col[0])
33
+ self._headers.append(col[0].lower())
34
+ elif hasattr(col, 'name'): # Object with name attribute
35
+ self._headers.append(col.name.lower())
36
+ else:
37
+ self._headers.append(f'column_{len(self._headers)}')
38
+ except (AttributeError, TypeError, IndexError):
39
+ self._headers = []
40
+
41
+ self.__as_strings = False
42
+ self.__enumerate = False
43
+ self.__count = -1
44
+ self.__columns = {}
45
+ self.__tx = tx
46
+ self.__sql = sql
47
+ self.__params = params
48
+ self.transform = lambda row: dict(zip(self.headers, row)) # Default transform
49
+ self._cached_first_row = None
50
+ self._first_row_fetched = False
51
+ self._exhausted = False
52
+
53
+ # Pre-fetch the first row to enable immediate boolean evaluation
54
+ self._fetch_first_row()
55
+
56
+ def _fetch_first_row(self):
57
+ """
58
+ Pre-fetch the first row from the cursor to enable immediate boolean evaluation.
59
+ Only attempts to fetch for SELECT-like operations that return rows.
60
+ """
61
+ if self._first_row_fetched or not self._cursor:
62
+ return
63
+
64
+ # Don't try to fetch from INSERT/UPDATE/DELETE operations
65
+ # These operations don't return rows, only rowcount
66
+ if self.__sql and self.__sql.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE')):
67
+ self._exhausted = True
68
+ self._first_row_fetched = True
69
+ return
70
+
71
+ try:
72
+ raw_row = self._cursor.fetchone()
73
+ if raw_row:
74
+ self._cached_first_row = raw_row
75
+ else:
76
+ self._exhausted = True
77
+ except Exception:
78
+ # If there's an error fetching (e.g., cursor closed), assume no results
79
+ self._exhausted = True
80
+ self._cursor = None # Mark cursor as invalid
81
+ finally:
82
+ self._first_row_fetched = True
83
+
84
+ def __str__(self):
85
+ return repr(self.all())
86
+
87
+ def __enter__(self):
88
+ return self
89
+
90
+ def __exit__(self, exc_type, exc_val, exc_tb):
91
+ if not exc_type:
92
+ self.close()
93
+
94
+ def __bool__(self):
95
+ """
96
+ Return True if there are more rows available to fetch.
97
+ This changes as rows are consumed - after the last row is fetched, this becomes False.
98
+ """
99
+ return self.has_results()
100
+
101
+ def is_empty(self):
102
+ """
103
+ Return True if there are no more rows available to fetch.
104
+ """
105
+ return not self.has_results()
106
+
107
+ def has_results(self):
108
+ """
109
+ Return True if there are more rows available to fetch.
110
+ This is based on whether we have a cached row or the cursor isn't exhausted.
111
+ """
112
+ return self._cached_first_row is not None or (not self._exhausted and self._cursor)
113
+
114
+ def __next__(self):
115
+ """
116
+ Iterator interface to retrieve the next row.
117
+ """
118
+ # If we have a cached first row, return it and clear the cache
119
+ if self._cached_first_row is not None:
120
+ row = self._cached_first_row
121
+ self._cached_first_row = None
122
+ # Try to pre-fetch the next row to update our state
123
+ self._try_cache_next_row()
124
+ elif not self._exhausted and self._cursor:
125
+ try:
126
+ row = self._cursor.fetchone()
127
+ if not row:
128
+ self._exhausted = True
129
+ raise StopIteration
130
+ # Try to pre-fetch the next row to update our state
131
+ self._try_cache_next_row()
132
+ except Exception as e:
133
+ # Handle cursor errors (e.g., closed cursor)
134
+ self._exhausted = True
135
+ self._cursor = None
136
+ if isinstance(e, StopIteration):
137
+ raise
138
+ raise StopIteration
139
+ else:
140
+ raise StopIteration
141
+
142
+ # Apply transformations
143
+ if self.__as_strings:
144
+ row = ["" if x is None else str(x) for x in row]
145
+ if self.__enumerate:
146
+ self.__count += 1
147
+ return (self.__count, self.transform(row))
148
+ return self.transform(row)
149
+
150
+ def _try_cache_next_row(self):
151
+ """
152
+ Try to cache the next row to maintain accurate boolean state.
153
+ This is called after we return a row to check if there are more.
154
+ """
155
+ if not self._cursor or self._cached_first_row is not None:
156
+ return
157
+
158
+ try:
159
+ next_row = self._cursor.fetchone()
160
+ if next_row:
161
+ self._cached_first_row = next_row
162
+ else:
163
+ self._exhausted = True
164
+ except Exception:
165
+ # If cursor is closed or has error, mark as exhausted
166
+ self._exhausted = True
167
+ self._cursor = None
168
+
169
+ def batch(self, qty=1):
170
+ """
171
+ Yields lists (batches) of rows with size = qty until no rows remain.
172
+ """
173
+ results = []
174
+ while True:
175
+ try:
176
+ results.append(next(self))
177
+ except StopIteration:
178
+ if results:
179
+ yield results
180
+ break
181
+ if len(results) == qty:
182
+ yield results
183
+ results = []
184
+
185
+ def all(self):
186
+ """
187
+ Retrieves all rows at once into a list.
188
+ """
189
+ results = []
190
+ while True:
191
+ try:
192
+ results.append(next(self))
193
+ except StopIteration:
194
+ break
195
+ return results
196
+
197
+ def __iter__(self):
198
+ return self
199
+
200
+ @property
201
+ def headers(self):
202
+ """
203
+ Retrieves column headers from the cursor if not already set.
204
+ """
205
+ if not self._headers and self._cursor and hasattr(self._cursor, "description"):
206
+ self._headers = [x[0].lower() for x in self._cursor.description]
207
+ return self._headers
208
+
209
+ @property
210
+ def columns(self):
211
+ """
212
+ Retrieves detailed column information from the cursor.
213
+ Gracefully handles different database types.
214
+ """
215
+ if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
216
+ for column in self._cursor.description:
217
+ data = {
218
+ "type_name": "unknown" # Default value
219
+ }
220
+
221
+ # Try to get type information (PostgreSQL specific)
222
+ try:
223
+ if hasattr(column, 'type_code') and self.__tx and hasattr(self.__tx, 'pg_types'):
224
+ data["type_name"] = self.__tx.pg_types.get(column.type_code, "unknown")
225
+ except (AttributeError, KeyError):
226
+ # Keep default value
227
+ pass
228
+
229
+ # Get all other column attributes safely
230
+ for key in dir(column):
231
+ if not key.startswith("__"):
232
+ try:
233
+ data[key] = getattr(column, key)
234
+ except (AttributeError, TypeError):
235
+ # Skip attributes that can't be accessed
236
+ continue
237
+
238
+ column_name = getattr(column, 'name', f'column_{len(self.__columns)}')
239
+ self.__columns[column_name] = data
240
+ return self.__columns
241
+
242
+ @property
243
+ def cursor(self):
244
+ return self._cursor
245
+
246
+ def close(self):
247
+ """
248
+ Closes the underlying cursor if it exists and marks result as exhausted.
249
+ """
250
+ if self._cursor:
251
+ try:
252
+ self._cursor.close()
253
+ except Exception:
254
+ # Ignore errors when closing cursor
255
+ pass
256
+ finally:
257
+ self._cursor = None
258
+ # Mark as exhausted and clear cached data
259
+ self._exhausted = True
260
+ self._cached_first_row = None
261
+
262
+ def as_dict(self):
263
+ """
264
+ Transform each row into a dictionary keyed by column names.
265
+ """
266
+ self.transform = lambda row: dict(zip(self.headers, row))
267
+ return self
268
+
269
+ def as_json(self):
270
+ """
271
+ Transform each row into JSON (string).
272
+ """
273
+ self.transform = lambda row: to_json(dict(zip(self.headers, row)))
274
+ return self
275
+
276
+ def as_named_tuple(self):
277
+ """
278
+ Transform each row into a list of (column_name, value) pairs.
279
+ """
280
+ self.transform = lambda row: list(zip(self.headers, row))
281
+ return self
282
+
283
+ def as_list(self):
284
+ """
285
+ Transform each row into a list of values.
286
+ """
287
+ self.transform = lambda row: list(row)
288
+ return self
289
+
290
+ def as_tuple(self):
291
+ """
292
+ Transform each row into a tuple of values.
293
+ """
294
+ self.transform = lambda row: row
295
+ return self
296
+
297
+ def as_simple_list(self, pos=0):
298
+ """
299
+ Transform each row into the single value at position `pos`.
300
+ """
301
+ self.transform = lambda row: row[pos]
302
+ return self
303
+
304
+ def strings(self, as_strings=True):
305
+ """
306
+ Indicate whether retrieved rows should be coerced to string form.
307
+ """
308
+ self.__as_strings = as_strings
309
+ return self
310
+
311
+ def scalar(self, default=None):
312
+ """
313
+ Return the first column of the first row, or `default` if no rows.
314
+ After calling this method, the result is marked as exhausted.
315
+ """
316
+ if self._cached_first_row is not None:
317
+ val = self._cached_first_row
318
+ self._cached_first_row = None
319
+ self._exhausted = True # Mark as exhausted since we only want one value
320
+ return val[0] if val else default
321
+ elif not self._exhausted and self._cursor:
322
+ try:
323
+ val = self._cursor.fetchone()
324
+ self._exhausted = True
325
+ return val[0] if val else default
326
+ except Exception:
327
+ # If cursor error, return default
328
+ self._exhausted = True
329
+ self._cursor = None
330
+ return default
331
+ return default
332
+
333
+ def one(self, default=None):
334
+ """
335
+ Return the first row or `default` if no rows.
336
+ After calling this method, the result is marked as exhausted.
337
+ """
338
+ try:
339
+ row = next(self)
340
+ # Mark as exhausted since we only want one row
341
+ self._exhausted = True
342
+ self._cached_first_row = None # Clear any cached row
343
+ return row
344
+ except StopIteration:
345
+ return default
346
+
347
+ def get_table_data(self, headers=True):
348
+ """
349
+ Builds a two-dimensional list: first row is column headers, subsequent rows are data.
350
+ """
351
+ self.as_list()
352
+ rows = []
353
+ for row in self:
354
+ row = ["" if x is None else str(x) for x in row]
355
+ rows.append(row)
356
+ if isinstance(headers, list):
357
+ rows.insert(0, [x.replace("_", " ").title() for x in headers])
358
+ elif headers:
359
+ rows.insert(0, [x.replace("_", " ").title() for x in self.headers])
360
+ return rows
361
+
362
+ def enum(self):
363
+ """
364
+ Yields each row as (row_index, transformed_row).
365
+ """
366
+ self.__enumerate = True
367
+ return self
368
+
369
+ @property
370
+ def sql(self):
371
+ return self.__sql
372
+
373
+ @property
374
+ def params(self):
375
+ return self.__params
@@ -1,4 +1,5 @@
1
1
  import pprint
2
+ from velocity.db.exceptions import DbColumnMissingError
2
3
 
3
4
 
4
5
  class Row:
@@ -85,10 +86,21 @@ class Row:
85
86
  return list(d.items())
86
87
 
87
88
  def get(self, key, failobj=None):
88
- data = self[key]
89
- if data is None:
89
+ try:
90
+ data = self[key]
91
+ if data is None:
92
+ return failobj
93
+ return data
94
+ except DbColumnMissingError:
95
+ # Column doesn't exist in the table, return the default value
90
96
  return failobj
91
- return data
97
+ except Exception as e:
98
+ # Check if the error message indicates a missing column
99
+ error_msg = str(e).lower()
100
+ if 'column' in error_msg and ('does not exist' in error_msg or 'not found' in error_msg):
101
+ return failobj
102
+ # Re-raise other exceptions
103
+ raise
92
104
 
93
105
  def setdefault(self, key, default=None):
94
106
  data = self[key]