velocity-python 0.0.104__py3-none-any.whl → 0.0.106__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.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

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,40 @@ 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
+ 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
23
83
 
24
84
  def __str__(self):
25
85
  return repr(self.all())
@@ -31,20 +91,80 @@ class Result:
31
91
  if not exc_type:
32
92
  self.close()
33
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
+
34
114
  def __next__(self):
35
115
  """
36
116
  Iterator interface to retrieve the next row.
37
117
  """
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
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
48
168
 
49
169
  def batch(self, qty=1):
50
170
  """
@@ -90,16 +210,33 @@ class Result:
90
210
  def columns(self):
91
211
  """
92
212
  Retrieves detailed column information from the cursor.
213
+ Gracefully handles different database types.
93
214
  """
94
215
  if not self.__columns and self._cursor and hasattr(self._cursor, "description"):
95
216
  for column in self._cursor.description:
96
217
  data = {
97
- "type_name": self.__tx.pg_types[column.type_code],
218
+ "type_name": "unknown" # Default value
98
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
99
230
  for key in dir(column):
100
- if "__" not in key:
101
- data[key] = getattr(column, key)
102
- self.__columns[column.name] = data
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
103
240
  return self.__columns
104
241
 
105
242
  @property
@@ -108,10 +245,19 @@ class Result:
108
245
 
109
246
  def close(self):
110
247
  """
111
- Closes the underlying cursor if it exists.
248
+ Closes the underlying cursor if it exists and marks result as exhausted.
112
249
  """
113
250
  if self._cursor:
114
- self._cursor.close()
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
115
261
 
116
262
  def as_dict(self):
117
263
  """
@@ -165,23 +311,35 @@ class Result:
165
311
  def scalar(self, default=None):
166
312
  """
167
313
  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
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
175
332
 
176
333
  def one(self, default=None):
177
334
  """
178
335
  Return the first row or `default` if no rows.
336
+ After calling this method, the result is marked as exhausted.
179
337
  """
180
338
  try:
181
339
  row = next(self)
182
- # Drain remaining.
183
- if self._cursor:
184
- self._cursor.fetchall()
340
+ # Mark as exhausted since we only want one row
341
+ self._exhausted = True
342
+ self._cached_first_row = None # Clear any cached row
185
343
  return row
186
344
  except StopIteration:
187
345
  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):
@@ -430,7 +431,7 @@ class Table:
430
431
  if kwds.get("sql_only", False):
431
432
  return sql, vals
432
433
  result = self.tx.execute(sql, vals, cursor=self.cursor())
433
- return result.cursor.rowcount
434
+ return result.cursor.rowcount if result.cursor else 0
434
435
 
435
436
  @reset_id_on_dup_key
436
437
  @create_missing
@@ -442,7 +443,7 @@ class Table:
442
443
  if kwds.get("sql_only", False):
443
444
  return sql, vals
444
445
  result = self.tx.execute(sql, vals, cursor=self.cursor())
445
- return result.cursor.rowcount
446
+ return result.cursor.rowcount if result.cursor else 0
446
447
 
447
448
  @reset_id_on_dup_key
448
449
  @create_missing
@@ -461,7 +462,7 @@ class Table:
461
462
  if kwds.get("sql_only", False):
462
463
  return sql, vals
463
464
  result = self.tx.execute(sql, vals, cursor=self.cursor())
464
- return result.cursor.rowcount
465
+ return result.cursor.rowcount if result.cursor else 0
465
466
 
466
467
  upsert = merge
467
468
  indate = merge
@@ -661,7 +662,7 @@ class Table:
661
662
  if kwds.get("sql_only", False):
662
663
  return sql, vals
663
664
  result = self.tx.execute(sql, vals)
664
- return result.cursor.rowcount
665
+ return result.cursor.rowcount if result.cursor else 0
665
666
 
666
667
  def truncate(self, **kwds):
667
668
  """
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.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
@@ -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=dgiOXH-iJXuDH4PbSTWVkn-heAkJQcXCC-gs0ZuqF94,12814
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=oQFam6DwLnGiqa6v8Wys_yD_N4iWwfxGBwgLB5VUQSQ,34682
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.104.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
52
- velocity_python-0.0.104.dist-info/METADATA,sha256=YOI2c5K-WMI5xOHWO6iJtSaksAGrxVc67m6XNkt3q4A,33023
53
- velocity_python-0.0.104.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- velocity_python-0.0.104.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
55
- velocity_python-0.0.104.dist-info/RECORD,,
52
+ velocity_python-0.0.106.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
53
+ velocity_python-0.0.106.dist-info/METADATA,sha256=2KmlGjeomKYirsh2YHunDlWp2ePlcMlNfxOLtAb0tcg,34262
54
+ velocity_python-0.0.106.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ velocity_python-0.0.106.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
56
+ velocity_python-0.0.106.dist-info/RECORD,,