velocity-python 0.0.103__py3-none-any.whl → 0.0.105__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/aws/amplify.py CHANGED
@@ -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
  },
velocity/db/__init__.py CHANGED
@@ -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
@@ -7,11 +7,37 @@ class Result:
7
7
  """
8
8
  Wraps a database cursor to provide various convenience transformations
9
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)
10
23
  """
11
24
 
12
25
  def __init__(self, cursor=None, tx=None, sql=None, params=None):
13
26
  self._cursor = cursor
14
- self._headers = [x[0].lower() for x in getattr(cursor, "description", []) or []]
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
+
15
41
  self.__as_strings = False
16
42
  self.__enumerate = False
17
43
  self.__count = -1
@@ -20,6 +46,32 @@ class Result:
20
46
  self.__sql = sql
21
47
  self.__params = params
22
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
23
75
 
24
76
  def __str__(self):
25
77
  return repr(self.all())
@@ -31,20 +83,80 @@ class Result:
31
83
  if not exc_type:
32
84
  self.close()
33
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
+
34
106
  def __next__(self):
35
107
  """
36
108
  Iterator interface to retrieve the next row.
37
109
  """
38
- if self._cursor:
39
- row = self._cursor.fetchone()
40
- if row:
41
- if self.__as_strings:
42
- row = ["" if x is None else str(x) for x in row]
43
- if self.__enumerate:
44
- self.__count += 1
45
- return (self.__count, self.transform(row))
46
- return self.transform(row)
47
- raise StopIteration
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
48
160
 
49
161
  def batch(self, qty=1):
50
162
  """
@@ -90,16 +202,33 @@ class Result:
90
202
  def columns(self):
91
203
  """
92
204
  Retrieves detailed column information from the cursor.
205
+ Gracefully handles different database types.
93
206
  """
94
207
  if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
95
208
  for column in self._cursor.description:
96
209
  data = {
97
- "type_name": self.__tx.pg_types[column.type_code],
210
+ "type_name": "unknown" # Default value
98
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
99
222
  for key in dir(column):
100
- if "__" not in key:
101
- data[key] = getattr(column, key)
102
- self.__columns[column.name] = data
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
103
232
  return self.__columns
104
233
 
105
234
  @property
@@ -108,10 +237,19 @@ class Result:
108
237
 
109
238
  def close(self):
110
239
  """
111
- Closes the underlying cursor if it exists.
240
+ Closes the underlying cursor if it exists and marks result as exhausted.
112
241
  """
113
242
  if self._cursor:
114
- self._cursor.close()
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
115
253
 
116
254
  def as_dict(self):
117
255
  """
@@ -165,23 +303,35 @@ class Result:
165
303
  def scalar(self, default=None):
166
304
  """
167
305
  Return the first column of the first row, or `default` if no rows.
168
- """
169
- if not self._cursor:
170
- return None
171
- val = self._cursor.fetchone()
172
- # Drain any remaining rows.
173
- self._cursor.fetchall()
174
- return val[0] if val else default
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
175
324
 
176
325
  def one(self, default=None):
177
326
  """
178
327
  Return the first row or `default` if no rows.
328
+ After calling this method, the result is marked as exhausted.
179
329
  """
180
330
  try:
181
331
  row = next(self)
182
- # Drain remaining.
183
- if self._cursor:
184
- self._cursor.fetchall()
332
+ # Mark as exhausted since we only want one row
333
+ self._exhausted = True
334
+ self._cached_first_row = None # Clear any cached row
185
335
  return row
186
336
  except StopIteration:
187
337
  return default
velocity/db/core/row.py CHANGED
@@ -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]
velocity/db/core/table.py CHANGED
@@ -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):
velocity/db/utils.py ADDED
@@ -0,0 +1,209 @@
1
+ """
2
+ Database utility functions for common operations.
3
+
4
+ This module provides utility functions to handle common database operations
5
+ safely, including sorting with None values and other edge cases.
6
+ """
7
+
8
+ from typing import Any, Callable, List, Optional, Union
9
+
10
+
11
+ def safe_sort_key_none_last(field_name: str) -> Callable[[dict], tuple]:
12
+ """
13
+ Create a sort key function that places None values at the end.
14
+
15
+ Args:
16
+ field_name: Name of the field to sort by
17
+
18
+ Returns:
19
+ A function suitable for use as a sort key that handles None values
20
+
21
+ Example:
22
+ rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
23
+ sorted_rows = sorted(rows, key=safe_sort_key_none_last("date"))
24
+ # Result: [{"date": "2023-12"}, {"date": "2024-01"}, {"date": None}]
25
+ """
26
+ def sort_key(row: dict) -> tuple:
27
+ value = row.get(field_name)
28
+ if value is None:
29
+ return (1, "") # None values sort last
30
+ return (0, value)
31
+
32
+ return sort_key
33
+
34
+
35
+ def safe_sort_key_none_first(field_name: str) -> Callable[[dict], tuple]:
36
+ """
37
+ Create a sort key function that places None values at the beginning.
38
+
39
+ Args:
40
+ field_name: Name of the field to sort by
41
+
42
+ Returns:
43
+ A function suitable for use as a sort key that handles None values
44
+
45
+ Example:
46
+ rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
47
+ sorted_rows = sorted(rows, key=safe_sort_key_none_first("date"))
48
+ # Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
49
+ """
50
+ def sort_key(row: dict) -> tuple:
51
+ value = row.get(field_name)
52
+ if value is None:
53
+ return (0, "") # None values sort first
54
+ return (1, value)
55
+
56
+ return sort_key
57
+
58
+
59
+ def safe_sort_key_with_default(field_name: str, default_value: Any = "") -> Callable[[dict], Any]:
60
+ """
61
+ Create a sort key function that replaces None values with a default.
62
+
63
+ Args:
64
+ field_name: Name of the field to sort by
65
+ default_value: Value to use for None entries
66
+
67
+ Returns:
68
+ A function suitable for use as a sort key that handles None values
69
+
70
+ Example:
71
+ rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
72
+ sorted_rows = sorted(rows, key=safe_sort_key_with_default("date", "1900-01"))
73
+ # Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
74
+ """
75
+ def sort_key(row: dict) -> Any:
76
+ value = row.get(field_name)
77
+ return default_value if value is None else value
78
+
79
+ return sort_key
80
+
81
+
82
+ def safe_sort_rows(rows: List[dict], field_name: str,
83
+ none_handling: str = "last",
84
+ default_value: Any = "",
85
+ reverse: bool = False) -> List[dict]:
86
+ """
87
+ Safely sort a list of dictionaries by a field that may contain None values.
88
+
89
+ Args:
90
+ rows: List of dictionaries to sort
91
+ field_name: Name of the field to sort by
92
+ none_handling: How to handle None values - "first", "last", or "default"
93
+ default_value: Default value to use when none_handling is "default"
94
+ reverse: Whether to reverse the sort order
95
+
96
+ Returns:
97
+ New list of dictionaries sorted by the specified field
98
+
99
+ Raises:
100
+ ValueError: If none_handling is not a valid option
101
+
102
+ Example:
103
+ rows = [
104
+ {"name": "Alice", "date": "2024-01"},
105
+ {"name": "Bob", "date": None},
106
+ {"name": "Charlie", "date": "2023-12"}
107
+ ]
108
+
109
+ # None values last
110
+ sorted_rows = safe_sort_rows(rows, "date")
111
+
112
+ # None values first
113
+ sorted_rows = safe_sort_rows(rows, "date", none_handling="first")
114
+
115
+ # Replace None with default
116
+ sorted_rows = safe_sort_rows(rows, "date", none_handling="default",
117
+ default_value="1900-01")
118
+ """
119
+ if none_handling == "last":
120
+ key_func = safe_sort_key_none_last(field_name)
121
+ elif none_handling == "first":
122
+ key_func = safe_sort_key_none_first(field_name)
123
+ elif none_handling == "default":
124
+ key_func = safe_sort_key_with_default(field_name, default_value)
125
+ else:
126
+ raise ValueError(f"Invalid none_handling option: {none_handling}. "
127
+ "Must be 'first', 'last', or 'default'")
128
+
129
+ return sorted(rows, key=key_func, reverse=reverse)
130
+
131
+
132
+ def group_by_fields(rows: List[dict], *field_names: str) -> dict:
133
+ """
134
+ Group rows by one or more field values.
135
+
136
+ Args:
137
+ rows: List of dictionaries to group
138
+ *field_names: Names of fields to group by
139
+
140
+ Returns:
141
+ Dictionary where keys are tuples of field values and values are lists of rows
142
+
143
+ Example:
144
+ rows = [
145
+ {"email": "alice@example.com", "type": "premium", "amount": 100},
146
+ {"email": "alice@example.com", "type": "basic", "amount": 50},
147
+ {"email": "bob@example.com", "type": "premium", "amount": 100},
148
+ ]
149
+
150
+ # Group by email only
151
+ groups = group_by_fields(rows, "email")
152
+ # Result: {
153
+ # ("alice@example.com",): [row1, row2],
154
+ # ("bob@example.com",): [row3]
155
+ # }
156
+
157
+ # Group by email and type
158
+ groups = group_by_fields(rows, "email", "type")
159
+ # Result: {
160
+ # ("alice@example.com", "premium"): [row1],
161
+ # ("alice@example.com", "basic"): [row2],
162
+ # ("bob@example.com", "premium"): [row3]
163
+ # }
164
+ """
165
+ groups = {}
166
+ for row in rows:
167
+ key = tuple(row.get(field) for field in field_names)
168
+ if key not in groups:
169
+ groups[key] = []
170
+ groups[key].append(row)
171
+
172
+ return groups
173
+
174
+
175
+ def safe_sort_grouped_rows(grouped_rows: dict, field_name: str,
176
+ none_handling: str = "last",
177
+ default_value: Any = "",
178
+ reverse: bool = False) -> dict:
179
+ """
180
+ Safely sort rows within each group of a grouped result.
181
+
182
+ Args:
183
+ grouped_rows: Dictionary of grouped rows (from group_by_fields)
184
+ field_name: Name of the field to sort by within each group
185
+ none_handling: How to handle None values - "first", "last", or "default"
186
+ default_value: Default value to use when none_handling is "default"
187
+ reverse: Whether to reverse the sort order
188
+
189
+ Returns:
190
+ Dictionary with the same keys but sorted lists as values
191
+
192
+ Example:
193
+ # After grouping payment profiles by email and card number
194
+ groups = group_by_fields(payment_profiles, "email_address", "card_number")
195
+
196
+ # Sort each group by expiration date, with None values last
197
+ sorted_groups = safe_sort_grouped_rows(groups, "expiration_date")
198
+
199
+ # Now process each sorted group
200
+ for group_key, sorted_group in sorted_groups.items():
201
+ for idx, row in enumerate(sorted_group):
202
+ # Process each row safely
203
+ pass
204
+ """
205
+ result = {}
206
+ for key, rows in grouped_rows.items():
207
+ result[key] = safe_sort_rows(rows, field_name, none_handling, default_value, reverse)
208
+
209
+ return result
@@ -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
@@ -5,24 +5,25 @@ velocity/app/orders.py,sha256=W-HAXEwY8-IFXbKh82HnMeRVZM7P-TWGEQOWtkLIzI4,6298
5
5
  velocity/app/payments.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  velocity/app/purchase_orders.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  velocity/aws/__init__.py,sha256=tj9-NliYxRVPYLnnDuA4FMwBHbbH4ed8gtHgwRskNgY,647
8
- velocity/aws/amplify.py,sha256=3W0o1eOzypLzzMezPRlGISyoGtc-anbMs9p9nVphHGc,15047
8
+ velocity/aws/amplify.py,sha256=n6ttzHmbF8tc0ZDN8LIA6s_fnkZ3ylu3AAUAYyD9MzE,15048
9
9
  velocity/aws/handlers/__init__.py,sha256=xnpFZJVlC2uoeeFW4zuPST8wA8ajaQDky5Y6iXZzi3A,172
10
10
  velocity/aws/handlers/context.py,sha256=UIjNR83y2NSIyK8HMPX8t5tpJHFNabiZvNgmmdQL3HA,1822
11
11
  velocity/aws/handlers/lambda_handler.py,sha256=0KrT6UIxDILzBRpoRSvwDgHpQ-vWfubcZFOCbJsewDc,6516
12
12
  velocity/aws/handlers/response.py,sha256=LXhtizLKnVBWjtHyE0h0bk-NYDrRpj7CHa7tRz9KkC4,9324
13
13
  velocity/aws/handlers/sqs_handler.py,sha256=nqt8NMOc5yO-L6CZo7NjgR8Q4KPKTDFBO-0eHu6oxkY,7149
14
- velocity/db/__init__.py,sha256=Xr-kN98AKgjDic1Lxi0yLiobsEpN5a6ZjDMS8KHSTlE,301
14
+ velocity/db/__init__.py,sha256=t7YJT42U19Vkd4MMz5MhU8IFryKfblNPLJcPqzpb4HQ,566
15
15
  velocity/db/exceptions.py,sha256=oTXzdxP0GrraGrqRD1JgIVP5urO5yNN7A3IzTiAtNJ0,2173
16
+ velocity/db/utils.py,sha256=3uUTLjMUC6g18bn7jRGBajYlXNFlBDZkGxIwQSBK_Y8,7298
16
17
  velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
18
19
  velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
19
20
  velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
20
- velocity/db/core/engine.py,sha256=uF0XC_kVTNQ2LdX1xaUPSCYYtGHyGSZ-qmhRtSQvSO8,49187
21
+ velocity/db/core/engine.py,sha256=iLQSr9hvn4w78EGiCbS2WsiojQgPYRBW_EGjTS2074g,49225
21
22
  velocity/db/core/exceptions.py,sha256=tuDniRqTX8Opc2d033LPJOI3Ux4NSwUcHqW729n-HXA,1027
22
- velocity/db/core/result.py,sha256=OVqoMwlx3CHNNwr-JGWRx5I8u_YX6hlUpecx99UT5nE,6164
23
- velocity/db/core/row.py,sha256=aliLYTTFirgJsOvmUsANwJMyxaATuhpGpFJhcu_twwY,6709
23
+ velocity/db/core/result.py,sha256=q5YvOP8_ozDdWJxLfFkGyEvF_ZqSn-WqI0qO5UgVBa4,12393
24
+ velocity/db/core/row.py,sha256=zZ3zZbWjZkZfYAYuZJLHFJ8jdXc7dYv8Iyv9Ut8W8tE,7261
24
25
  velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
25
- velocity/db/core/table.py,sha256=g2mq7_VzLBtxITAn47BbgMcUoJAy9XVP6ohzScNl_so,34573
26
+ velocity/db/core/table.py,sha256=yUZVKrQXgHBMWY5Mf-XmwgaNhW-HHqfg57PpUW_RXHQ,34586
26
27
  velocity/db/core/transaction.py,sha256=unjmVkkfb7D8Wow6V8V8aLaxUZo316i--ksZxc4-I1Q,6613
27
28
  velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  velocity/db/servers/mysql.py,sha256=qHwlB_Mg02R7QFjD5QvJCorYYiP50CqEiQyZVl3uYns,20914
@@ -48,8 +49,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
48
49
  velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
49
50
  velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
50
51
  velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
51
- velocity_python-0.0.103.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
52
- velocity_python-0.0.103.dist-info/METADATA,sha256=9FYOm2SrGrV7CJloPGfUn8upxC3zNSst9bFUXwVOEng,33023
53
- velocity_python-0.0.103.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- velocity_python-0.0.103.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
55
- velocity_python-0.0.103.dist-info/RECORD,,
52
+ velocity_python-0.0.105.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
53
+ velocity_python-0.0.105.dist-info/METADATA,sha256=TEKdozOuF6TjYw8yBGI57YZpr_bvGWAufFkdcRscBlI,34262
54
+ velocity_python-0.0.105.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ velocity_python-0.0.105.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
56
+ velocity_python-0.0.105.dist-info/RECORD,,