velocity-python 0.0.103__tar.gz → 0.0.105__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 (81) hide show
  1. {velocity_python-0.0.103/src/velocity_python.egg-info → velocity_python-0.0.105}/PKG-INFO +55 -4
  2. {velocity_python-0.0.103 → velocity_python-0.0.105}/README.md +52 -0
  3. {velocity_python-0.0.103 → velocity_python-0.0.105}/pyproject.toml +3 -4
  4. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/amplify.py +1 -1
  5. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/__init__.py +11 -0
  6. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/engine.py +7 -7
  7. velocity_python-0.0.105/src/velocity/db/core/result.py +367 -0
  8. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/row.py +15 -3
  9. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/table.py +2 -1
  10. velocity_python-0.0.105/src/velocity/db/utils.py +209 -0
  11. {velocity_python-0.0.103 → velocity_python-0.0.105/src/velocity_python.egg-info}/PKG-INFO +55 -4
  12. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity_python.egg-info/SOURCES.txt +5 -0
  13. velocity_python-0.0.105/tests/test_db_utils.py +195 -0
  14. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_oconv.py +13 -11
  15. velocity_python-0.0.105/tests/test_payment_profile_sorting.py +144 -0
  16. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_postgres.py +47 -42
  17. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_response.py +7 -0
  18. velocity_python-0.0.105/tests/test_result_caching.py +277 -0
  19. velocity_python-0.0.105/tests/test_row_get_missing_column.py +68 -0
  20. velocity_python-0.0.103/src/velocity/db/core/result.py +0 -217
  21. {velocity_python-0.0.103 → velocity_python-0.0.105}/LICENSE +0 -0
  22. {velocity_python-0.0.103 → velocity_python-0.0.105}/setup.cfg +0 -0
  23. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/__init__.py +0 -0
  24. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/app/__init__.py +0 -0
  25. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/app/invoices.py +0 -0
  26. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/app/orders.py +0 -0
  27. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/app/payments.py +0 -0
  28. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/app/purchase_orders.py +0 -0
  29. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/__init__.py +0 -0
  30. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/handlers/__init__.py +0 -0
  31. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/handlers/context.py +0 -0
  32. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  33. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/handlers/response.py +0 -0
  34. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  35. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/__init__.py +0 -0
  36. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/column.py +0 -0
  37. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/database.py +0 -0
  38. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/decorators.py +0 -0
  39. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/exceptions.py +0 -0
  40. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/sequence.py +0 -0
  41. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/core/transaction.py +0 -0
  42. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/exceptions.py +0 -0
  43. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/__init__.py +0 -0
  44. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/mysql.py +0 -0
  45. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/mysql_reserved.py +0 -0
  46. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/postgres/__init__.py +0 -0
  47. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/postgres/operators.py +0 -0
  48. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/postgres/reserved.py +0 -0
  49. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/postgres/sql.py +0 -0
  50. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/postgres/types.py +0 -0
  51. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/sqlite.py +0 -0
  52. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/sqlite_reserved.py +0 -0
  53. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/sqlserver.py +0 -0
  54. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
  55. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/db/servers/tablehelper.py +0 -0
  56. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/__init__.py +0 -0
  57. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/conv/__init__.py +0 -0
  58. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/conv/iconv.py +0 -0
  59. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/conv/oconv.py +0 -0
  60. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/db.py +0 -0
  61. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/export.py +0 -0
  62. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/format.py +0 -0
  63. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/mail.py +0 -0
  64. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/merge.py +0 -0
  65. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/timer.py +0 -0
  66. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity/misc/tools.py +0 -0
  67. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  68. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity_python.egg-info/requires.txt +0 -0
  69. {velocity_python-0.0.103 → velocity_python-0.0.105}/src/velocity_python.egg-info/top_level.txt +0 -0
  70. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_db.py +0 -0
  71. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_email_processing.py +0 -0
  72. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_fix.py +0 -0
  73. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_format.py +0 -0
  74. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_iconv.py +0 -0
  75. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_merge.py +0 -0
  76. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_original_error.py +0 -0
  77. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_process_error_robustness.py +0 -0
  78. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_spreadsheet_functions.py +0 -0
  79. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_sql_builder.py +0 -0
  80. {velocity_python-0.0.103 → velocity_python-0.0.105}/tests/test_tablehelper.py +0 -0
  81. {velocity_python-0.0.103 → velocity_python-0.0.105}/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.103
3
+ Version: 0.0.105
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.103"
7
+ version = "0.0.105"
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,367 @@
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
+ """
60
+ if self._first_row_fetched or not self._cursor:
61
+ return
62
+
63
+ try:
64
+ raw_row = self._cursor.fetchone()
65
+ if raw_row:
66
+ self._cached_first_row = raw_row
67
+ else:
68
+ self._exhausted = True
69
+ except Exception:
70
+ # If there's an error fetching (e.g., cursor closed), assume no results
71
+ self._exhausted = True
72
+ self._cursor = None # Mark cursor as invalid
73
+ finally:
74
+ self._first_row_fetched = True
75
+
76
+ def __str__(self):
77
+ return repr(self.all())
78
+
79
+ def __enter__(self):
80
+ return self
81
+
82
+ def __exit__(self, exc_type, exc_val, exc_tb):
83
+ if not exc_type:
84
+ self.close()
85
+
86
+ def __bool__(self):
87
+ """
88
+ Return True if there are more rows available to fetch.
89
+ This changes as rows are consumed - after the last row is fetched, this becomes False.
90
+ """
91
+ return self.has_results()
92
+
93
+ def is_empty(self):
94
+ """
95
+ Return True if there are no more rows available to fetch.
96
+ """
97
+ return not self.has_results()
98
+
99
+ def has_results(self):
100
+ """
101
+ Return True if there are more rows available to fetch.
102
+ This is based on whether we have a cached row or the cursor isn't exhausted.
103
+ """
104
+ return self._cached_first_row is not None or (not self._exhausted and self._cursor)
105
+
106
+ def __next__(self):
107
+ """
108
+ Iterator interface to retrieve the next row.
109
+ """
110
+ # If we have a cached first row, return it and clear the cache
111
+ if self._cached_first_row is not None:
112
+ row = self._cached_first_row
113
+ self._cached_first_row = None
114
+ # Try to pre-fetch the next row to update our state
115
+ self._try_cache_next_row()
116
+ elif not self._exhausted and self._cursor:
117
+ try:
118
+ row = self._cursor.fetchone()
119
+ if not row:
120
+ self._exhausted = True
121
+ raise StopIteration
122
+ # Try to pre-fetch the next row to update our state
123
+ self._try_cache_next_row()
124
+ except Exception as e:
125
+ # Handle cursor errors (e.g., closed cursor)
126
+ self._exhausted = True
127
+ self._cursor = None
128
+ if isinstance(e, StopIteration):
129
+ raise
130
+ raise StopIteration
131
+ else:
132
+ raise StopIteration
133
+
134
+ # Apply transformations
135
+ if self.__as_strings:
136
+ row = ["" if x is None else str(x) for x in row]
137
+ if self.__enumerate:
138
+ self.__count += 1
139
+ return (self.__count, self.transform(row))
140
+ return self.transform(row)
141
+
142
+ def _try_cache_next_row(self):
143
+ """
144
+ Try to cache the next row to maintain accurate boolean state.
145
+ This is called after we return a row to check if there are more.
146
+ """
147
+ if not self._cursor or self._cached_first_row is not None:
148
+ return
149
+
150
+ try:
151
+ next_row = self._cursor.fetchone()
152
+ if next_row:
153
+ self._cached_first_row = next_row
154
+ else:
155
+ self._exhausted = True
156
+ except Exception:
157
+ # If cursor is closed or has error, mark as exhausted
158
+ self._exhausted = True
159
+ self._cursor = None
160
+
161
+ def batch(self, qty=1):
162
+ """
163
+ Yields lists (batches) of rows with size = qty until no rows remain.
164
+ """
165
+ results = []
166
+ while True:
167
+ try:
168
+ results.append(next(self))
169
+ except StopIteration:
170
+ if results:
171
+ yield results
172
+ break
173
+ if len(results) == qty:
174
+ yield results
175
+ results = []
176
+
177
+ def all(self):
178
+ """
179
+ Retrieves all rows at once into a list.
180
+ """
181
+ results = []
182
+ while True:
183
+ try:
184
+ results.append(next(self))
185
+ except StopIteration:
186
+ break
187
+ return results
188
+
189
+ def __iter__(self):
190
+ return self
191
+
192
+ @property
193
+ def headers(self):
194
+ """
195
+ Retrieves column headers from the cursor if not already set.
196
+ """
197
+ if not self._headers and self._cursor and hasattr(self._cursor, "description"):
198
+ self._headers = [x[0].lower() for x in self._cursor.description]
199
+ return self._headers
200
+
201
+ @property
202
+ def columns(self):
203
+ """
204
+ Retrieves detailed column information from the cursor.
205
+ Gracefully handles different database types.
206
+ """
207
+ if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
208
+ for column in self._cursor.description:
209
+ data = {
210
+ "type_name": "unknown" # Default value
211
+ }
212
+
213
+ # Try to get type information (PostgreSQL specific)
214
+ try:
215
+ if hasattr(column, 'type_code') and self.__tx and hasattr(self.__tx, 'pg_types'):
216
+ data["type_name"] = self.__tx.pg_types.get(column.type_code, "unknown")
217
+ except (AttributeError, KeyError):
218
+ # Keep default value
219
+ pass
220
+
221
+ # Get all other column attributes safely
222
+ for key in dir(column):
223
+ if not key.startswith("__"):
224
+ try:
225
+ data[key] = getattr(column, key)
226
+ except (AttributeError, TypeError):
227
+ # Skip attributes that can't be accessed
228
+ continue
229
+
230
+ column_name = getattr(column, 'name', f'column_{len(self.__columns)}')
231
+ self.__columns[column_name] = data
232
+ return self.__columns
233
+
234
+ @property
235
+ def cursor(self):
236
+ return self._cursor
237
+
238
+ def close(self):
239
+ """
240
+ Closes the underlying cursor if it exists and marks result as exhausted.
241
+ """
242
+ if self._cursor:
243
+ try:
244
+ self._cursor.close()
245
+ except Exception:
246
+ # Ignore errors when closing cursor
247
+ pass
248
+ finally:
249
+ self._cursor = None
250
+ # Mark as exhausted and clear cached data
251
+ self._exhausted = True
252
+ self._cached_first_row = None
253
+
254
+ def as_dict(self):
255
+ """
256
+ Transform each row into a dictionary keyed by column names.
257
+ """
258
+ self.transform = lambda row: dict(zip(self.headers, row))
259
+ return self
260
+
261
+ def as_json(self):
262
+ """
263
+ Transform each row into JSON (string).
264
+ """
265
+ self.transform = lambda row: to_json(dict(zip(self.headers, row)))
266
+ return self
267
+
268
+ def as_named_tuple(self):
269
+ """
270
+ Transform each row into a list of (column_name, value) pairs.
271
+ """
272
+ self.transform = lambda row: list(zip(self.headers, row))
273
+ return self
274
+
275
+ def as_list(self):
276
+ """
277
+ Transform each row into a list of values.
278
+ """
279
+ self.transform = lambda row: list(row)
280
+ return self
281
+
282
+ def as_tuple(self):
283
+ """
284
+ Transform each row into a tuple of values.
285
+ """
286
+ self.transform = lambda row: row
287
+ return self
288
+
289
+ def as_simple_list(self, pos=0):
290
+ """
291
+ Transform each row into the single value at position `pos`.
292
+ """
293
+ self.transform = lambda row: row[pos]
294
+ return self
295
+
296
+ def strings(self, as_strings=True):
297
+ """
298
+ Indicate whether retrieved rows should be coerced to string form.
299
+ """
300
+ self.__as_strings = as_strings
301
+ return self
302
+
303
+ def scalar(self, default=None):
304
+ """
305
+ Return the first column of the first row, or `default` if no rows.
306
+ After calling this method, the result is marked as exhausted.
307
+ """
308
+ if self._cached_first_row is not None:
309
+ val = self._cached_first_row
310
+ self._cached_first_row = None
311
+ self._exhausted = True # Mark as exhausted since we only want one value
312
+ return val[0] if val else default
313
+ elif not self._exhausted and self._cursor:
314
+ try:
315
+ val = self._cursor.fetchone()
316
+ self._exhausted = True
317
+ return val[0] if val else default
318
+ except Exception:
319
+ # If cursor error, return default
320
+ self._exhausted = True
321
+ self._cursor = None
322
+ return default
323
+ return default
324
+
325
+ def one(self, default=None):
326
+ """
327
+ Return the first row or `default` if no rows.
328
+ After calling this method, the result is marked as exhausted.
329
+ """
330
+ try:
331
+ row = next(self)
332
+ # Mark as exhausted since we only want one row
333
+ self._exhausted = True
334
+ self._cached_first_row = None # Clear any cached row
335
+ return row
336
+ except StopIteration:
337
+ return default
338
+
339
+ def get_table_data(self, headers=True):
340
+ """
341
+ Builds a two-dimensional list: first row is column headers, subsequent rows are data.
342
+ """
343
+ self.as_list()
344
+ rows = []
345
+ for row in self:
346
+ row = ["" if x is None else str(x) for x in row]
347
+ rows.append(row)
348
+ if isinstance(headers, list):
349
+ rows.insert(0, [x.replace("_", " ").title() for x in headers])
350
+ elif headers:
351
+ rows.insert(0, [x.replace("_", " ").title() for x in self.headers])
352
+ return rows
353
+
354
+ def enum(self):
355
+ """
356
+ Yields each row as (row_index, transformed_row).
357
+ """
358
+ self.__enumerate = True
359
+ return self
360
+
361
+ @property
362
+ def sql(self):
363
+ return self.__sql
364
+
365
+ @property
366
+ def params(self):
367
+ 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]
@@ -61,7 +61,8 @@ class Table:
61
61
  try:
62
62
  return self._cursor
63
63
  except:
64
- self._cursor = self.tx.cursor()
64
+ pass
65
+ self._cursor = self.tx.cursor()
65
66
  return self._cursor
66
67
 
67
68
  def __call__(self, where=None):