velocity-python 0.0.218__tar.gz → 0.0.220__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 (157) hide show
  1. {velocity_python-0.0.218/src/velocity_python.egg-info → velocity_python-0.0.220}/PKG-INFO +1 -1
  2. {velocity_python-0.0.218 → velocity_python-0.0.220}/pyproject.toml +1 -1
  3. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/decorators.py +12 -0
  5. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/result.py +11 -1
  6. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/row.py +140 -64
  7. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/sequence.py +6 -5
  8. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/table.py +40 -87
  9. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/sql.py +48 -0
  10. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/sql.py +91 -0
  11. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_row_comprehensive.py +6 -9
  12. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_row_get_missing_column.py +16 -4
  13. {velocity_python-0.0.218 → velocity_python-0.0.220/src/velocity_python.egg-info}/PKG-INFO +1 -1
  14. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  15. velocity_python-0.0.220/tests/test_payment_braintree_adapter.py +77 -0
  16. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_payment_profiles.py +30 -1
  17. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_payment_router.py +47 -1
  18. velocity_python-0.0.220/tests/test_payment_stripe_adapter.py +186 -0
  19. {velocity_python-0.0.218 → velocity_python-0.0.220}/LICENSE +0 -0
  20. {velocity_python-0.0.218 → velocity_python-0.0.220}/README.md +0 -0
  21. {velocity_python-0.0.218 → velocity_python-0.0.220}/setup.cfg +0 -0
  22. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/__init__.py +0 -0
  23. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/invoices.py +0 -0
  24. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/orders.py +0 -0
  25. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/payments.py +0 -0
  26. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/purchase_orders.py +0 -0
  27. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/__init__.py +0 -0
  28. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_email_processing.py +0 -0
  29. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  30. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  31. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/__init__.py +0 -0
  32. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/amplify.py +0 -0
  33. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/amplify_build.py +0 -0
  34. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/__init__.py +0 -0
  35. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/base_handler.py +0 -0
  36. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/context.py +0 -0
  37. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/context_factory.py +0 -0
  38. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/exceptions.py +0 -0
  39. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  40. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  41. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  42. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  43. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/perf.py +0 -0
  44. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/response.py +0 -0
  45. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  46. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/__init__.py +0 -0
  47. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  48. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  49. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_response.py +0 -0
  50. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/__init__.py +0 -0
  51. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/__init__.py +0 -0
  52. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/column.py +0 -0
  53. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/database.py +0 -0
  54. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/engine.py +0 -0
  55. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/transaction.py +0 -0
  56. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/view.py +0 -0
  57. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/exceptions.py +0 -0
  58. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/__init__.py +0 -0
  59. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/__init__.py +0 -0
  60. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/initializer.py +0 -0
  61. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/operators.py +0 -0
  62. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/types.py +0 -0
  63. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/__init__.py +0 -0
  64. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/operators.py +0 -0
  65. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/reserved.py +0 -0
  66. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/sql.py +0 -0
  67. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/types.py +0 -0
  68. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/__init__.py +0 -0
  69. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/operators.py +0 -0
  70. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/reserved.py +0 -0
  71. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/types.py +0 -0
  72. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  73. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/operators.py +0 -0
  74. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  75. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/sql.py +0 -0
  76. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/types.py +0 -0
  77. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  78. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  79. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  80. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  81. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/types.py +0 -0
  82. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/tablehelper.py +0 -0
  83. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/__init__.py +0 -0
  84. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/common_db_test.py +0 -0
  85. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/__init__.py +0 -0
  86. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/common.py +0 -0
  87. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_column.py +0 -0
  88. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  89. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_database.py +0 -0
  90. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  91. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  92. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  93. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_result.py +0 -0
  94. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_row.py +0 -0
  95. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  96. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  97. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  98. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  99. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_table.py +0 -0
  100. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  101. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  102. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/__init__.py +0 -0
  103. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/common.py +0 -0
  104. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  105. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  106. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  107. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_db_utils.py +0 -0
  108. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_postgres.py +0 -0
  109. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  110. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  111. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_result_caching.py +0 -0
  112. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  113. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  114. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  115. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_sql_builder.py +0 -0
  116. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_tablehelper.py +0 -0
  117. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_view_helper.py +0 -0
  118. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/utils.py +0 -0
  119. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/logging.py +0 -0
  120. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/__init__.py +0 -0
  121. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/__init__.py +0 -0
  122. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/iconv.py +0 -0
  123. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/oconv.py +0 -0
  124. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/db.py +0 -0
  125. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/export.py +0 -0
  126. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/format.py +0 -0
  127. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/mail.py +0 -0
  128. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/merge.py +0 -0
  129. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/__init__.py +0 -0
  130. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_db.py +0 -0
  131. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_fix.py +0 -0
  132. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_format.py +0 -0
  133. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_iconv.py +0 -0
  134. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_merge.py +0 -0
  135. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_oconv.py +0 -0
  136. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_original_error.py +0 -0
  137. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_timer.py +0 -0
  138. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/timer.py +0 -0
  139. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tools.py +0 -0
  140. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/__init__.py +0 -0
  141. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/base_adapter.py +0 -0
  142. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/braintree_adapter.py +0 -0
  143. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/profiles.py +0 -0
  144. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/router.py +0 -0
  145. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/stripe_adapter.py +0 -0
  146. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  147. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/requires.txt +0 -0
  148. {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/top_level.txt +0 -0
  149. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_amplify_build.py +0 -0
  150. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_decorators.py +0 -0
  151. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_iconv_money_to_cents.py +0 -0
  152. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_lambda_handler.py +0 -0
  153. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_lambda_handler_auth.py +0 -0
  154. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_mixins_import.py +0 -0
  155. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  156. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_table_alter.py +0 -0
  157. {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.218
3
+ Version: 0.0.220
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.218"
7
+ version = "0.0.220"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.218"
1
+ __version__ = version = "0.0.220"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -1,8 +1,11 @@
1
+ import logging
1
2
  import time
2
3
  import random
3
4
  from functools import wraps
4
5
  from velocity.db import exceptions
5
6
 
7
+ logger = logging.getLogger("velocity.db")
8
+
6
9
 
7
10
  _PRIMARY_KEY_PATTERNS = (
8
11
  "primary key",
@@ -108,6 +111,15 @@ def return_default(
108
111
  except func.exceptions as e:
109
112
  self.tx.rollback_savepoint(sp, cursor=self.cursor())
110
113
 
114
+ # Log the swallowed exception so silent failures are visible.
115
+ logger.warning(
116
+ "@return_default swallowed %s in %s.%s: %s",
117
+ e.__class__.__name__,
118
+ func.__module__,
119
+ getattr(func, "__qualname__", func.__name__),
120
+ e,
121
+ )
122
+
111
123
  # Capture swallowed exceptions for upstream diagnostics.
112
124
  # This decorator intentionally returns a default value instead of
113
125
  # raising, but consumers (e.g. API handlers) may still want to
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from velocity.misc.format import to_json
2
3
 
3
4
 
@@ -279,13 +280,22 @@ class Result:
279
280
  self.transform = lambda row: to_json(dict(zip(self.headers, row)))
280
281
  return self
281
282
 
282
- def as_named_tuple(self):
283
+ def as_pairs(self):
283
284
  """
284
285
  Transform each row into a list of (column_name, value) pairs.
285
286
  """
286
287
  self.transform = lambda row: list(zip(self.headers, row))
287
288
  return self
288
289
 
290
+ def as_named_tuple(self):
291
+ """Deprecated: use as_pairs() instead."""
292
+ warnings.warn(
293
+ "Result.as_named_tuple() is deprecated, use Result.as_pairs() instead.",
294
+ DeprecationWarning,
295
+ stacklevel=2,
296
+ )
297
+ return self.as_pairs()
298
+
289
299
  def as_list(self):
290
300
  """
291
301
  Transform each row into a list of values.
@@ -1,16 +1,32 @@
1
1
  import pprint
2
+ import warnings
3
+ from collections.abc import MutableMapping
2
4
  from velocity.db.exceptions import DbColumnMissingError
3
5
 
4
6
 
5
- class Row:
7
+ # Attributes that live on the Row instance itself and must never be
8
+ # intercepted by __getattr__ / __setattr__.
9
+ _INTERNAL_ATTRS = frozenset({
10
+ "table", "pk", "_cache", "_column_set",
11
+ })
12
+
13
+
14
+ class Row(MutableMapping):
6
15
  """
7
- Represents a single row in a given table, identified by a primary key or a dictionary of conditions.
16
+ Represents a single row in a given table, identified by a primary key.
17
+
18
+ Acts as a ``MutableMapping`` so that standard dict idioms work:
19
+ ``dict(row)``, ``{**row}``, ``for k in row:``, ``json.dumps(row)``.
20
+
21
+ Data is fetched from the database **once** on first access and cached
22
+ locally. Call ``row.refresh()`` to re-fetch. Writes are still
23
+ write-through (immediate UPDATE) and also update the local cache.
8
24
  """
9
25
 
10
26
  def __init__(self, table, key, lock=None):
11
27
  if isinstance(table, str):
12
28
  raise Exception("Table parameter must be a `table` instance.")
13
- self.table = table
29
+ object.__setattr__(self, "table", table)
14
30
 
15
31
  if isinstance(key, (dict, Row)):
16
32
  pk = {}
@@ -18,38 +34,68 @@ class Row:
18
34
  for k in self.key_cols:
19
35
  pk[k] = key[k]
20
36
  except KeyError:
21
- pk = key
37
+ pk = dict(key) if isinstance(key, Row) else key
22
38
  else:
23
39
  pk = {self.key_cols[0]: key}
24
40
 
25
- self.pk = pk
26
- self.cache = key
41
+ object.__setattr__(self, "pk", pk)
42
+ object.__setattr__(self, "_cache", None)
43
+ object.__setattr__(self, "_column_set", None)
27
44
  if lock:
28
45
  self.lock()
29
46
 
30
- def __repr__(self):
31
- return repr(self.to_dict())
47
+ # ------------------------------------------------------------------
48
+ # Cache management
49
+ # ------------------------------------------------------------------
50
+
51
+ def _ensure_cache(self):
52
+ """Populate the local cache from the database if not yet loaded."""
53
+ if self._cache is None:
54
+ data = self.table.select(where=self.pk).as_dict().one()
55
+ object.__setattr__(self, "_cache", data or {})
56
+ return self._cache
57
+
58
+ def _column_names_lower(self):
59
+ """Return a frozenset of lowered column names (cached)."""
60
+ if self._column_set is None:
61
+ cols = frozenset(k.lower() for k in self._ensure_cache())
62
+ object.__setattr__(self, "_column_set", cols)
63
+ return self._column_set
64
+
65
+ def refresh(self):
66
+ """Re-fetch the row data from the database, clearing the local cache."""
67
+ object.__setattr__(self, "_cache", None)
68
+ object.__setattr__(self, "_column_set", None)
69
+ self._ensure_cache()
70
+ return self
32
71
 
33
- def __str__(self):
34
- return pprint.pformat(self.to_dict())
72
+ def invalidate(self):
73
+ """Clear the local cache so the next access re-fetches from the database."""
74
+ object.__setattr__(self, "_cache", None)
75
+ object.__setattr__(self, "_column_set", None)
76
+ return self
35
77
 
36
- def __len__(self):
37
- return int(self.table.count(self.pk))
78
+ # ------------------------------------------------------------------
79
+ # MutableMapping protocol
80
+ # ------------------------------------------------------------------
38
81
 
39
82
  def __getitem__(self, key):
40
83
  if key in self.pk:
41
84
  return self.pk[key]
85
+ cache = self._ensure_cache()
86
+ if key in cache:
87
+ return cache[key]
88
+ # Fall back to a direct DB fetch for columns not in the initial SELECT
42
89
  return self.table.get_value(key, self.pk)
43
90
 
44
91
  def __setitem__(self, key, val):
45
92
  if key in self.pk:
46
93
  raise Exception("Cannot update a primary key.")
47
- if hasattr(self.table, "updins"):
48
- self.table.updins({key: val}, pk=self.pk)
49
- elif hasattr(self.table, "upsert"):
50
- self.table.upsert({key: val}, pk=self.pk)
51
- else:
52
- self.table.update({key: val}, pk=self.pk)
94
+ self.table.update_or_insert({key: val}, pk=self.pk)
95
+ # Update local cache
96
+ if self._cache is not None:
97
+ self._cache[key] = val
98
+ object.__setattr__(self, "_column_set", None)
53
99
 
54
100
  def __delitem__(self, key):
55
101
  if key in self.pk:
@@ -58,37 +104,91 @@ class Row:
58
104
  return
59
105
  self[key] = None
60
106
 
107
+ def __iter__(self):
108
+ return iter(self._ensure_cache())
109
+
110
+ def __len__(self):
111
+ return len(self._ensure_cache())
112
+
61
113
  def __contains__(self, key):
62
- return key.lower() in [x.lower() for x in self.keys()]
114
+ return key.lower() in self._column_names_lower()
115
+
116
+ # ------------------------------------------------------------------
117
+ # Attribute-style access (row.name instead of row['name'])
118
+ # ------------------------------------------------------------------
119
+
120
+ def __getattr__(self, key):
121
+ if key.startswith("_") or key in _INTERNAL_ATTRS:
122
+ raise AttributeError(key)
123
+ try:
124
+ return self[key]
125
+ except (KeyError, DbColumnMissingError):
126
+ raise AttributeError(
127
+ f"'{type(self).__name__}' object has no attribute '{key}'"
128
+ )
129
+
130
+ def __setattr__(self, key, val):
131
+ if key in _INTERNAL_ATTRS or key.startswith("_"):
132
+ object.__setattr__(self, key, val)
133
+ else:
134
+ self[key] = val
135
+
136
+ # ------------------------------------------------------------------
137
+ # Equality and hashing — based on (table name, pk)
138
+ # ------------------------------------------------------------------
139
+
140
+ def __eq__(self, other):
141
+ if isinstance(other, Row):
142
+ return self.table.name == other.table.name and self.pk == other.pk
143
+ if isinstance(other, dict):
144
+ return self.to_dict() == other
145
+ return NotImplemented
146
+
147
+ def __hash__(self):
148
+ return hash((self.table.name, tuple(sorted(self.pk.items()))))
149
+
150
+ def __ne__(self, other):
151
+ result = self.__eq__(other)
152
+ if result is NotImplemented:
153
+ return result
154
+ return not result
155
+
156
+ # ------------------------------------------------------------------
157
+ # Representations
158
+ # ------------------------------------------------------------------
159
+
160
+ def __repr__(self):
161
+ return repr(self.to_dict())
162
+
163
+ def __str__(self):
164
+ return pprint.pformat(self.to_dict())
165
+
166
+ def __bool__(self):
167
+ return bool(self._ensure_cache())
168
+
169
+ # ------------------------------------------------------------------
170
+ # dict-like helpers
171
+ # ------------------------------------------------------------------
63
172
 
64
173
  def clear(self):
65
174
  """
66
175
  Deletes this row from the database.
67
176
  """
68
177
  self.table.delete(where=self.pk)
178
+ self.invalidate()
69
179
  return self
70
180
 
71
181
  def keys(self):
72
- """
73
- Returns the column names in the table (including sys_ columns).
74
- """
75
- return self.table.sys_columns()
182
+ return self._ensure_cache().keys()
76
183
 
77
184
  def values(self, *args):
78
- """
79
- Returns values from this row, optionally restricted to columns in `args`.
80
- """
81
- d = self.table.select(where=self.pk).as_dict().one()
185
+ d = self._ensure_cache()
82
186
  if args:
83
187
  return [d[arg] for arg in args]
84
188
  return list(d.values())
85
189
 
86
190
  def items(self):
87
- """
88
- Returns (key, value) pairs for all columns.
89
- """
90
- d = self.table.select(where=self.pk).as_dict().one()
91
- return list(d.items())
191
+ return self._ensure_cache().items()
92
192
 
93
193
  def get(self, key, failobj=None):
94
194
  try:
@@ -97,16 +197,13 @@ class Row:
97
197
  return failobj
98
198
  return data
99
199
  except DbColumnMissingError:
100
- # Column doesn't exist in the table, return the default value
101
200
  return failobj
102
201
  except Exception as e:
103
- # Check if the error message indicates a missing column
104
202
  error_msg = str(e).lower()
105
203
  if "column" in error_msg and (
106
204
  "does not exist" in error_msg or "not found" in error_msg
107
205
  ):
108
206
  return failobj
109
- # Re-raise other exceptions
110
207
  raise
111
208
 
112
209
  def setdefault(self, key, default=None):
@@ -118,7 +215,7 @@ class Row:
118
215
 
119
216
  def update(self, dict_=None, **kwds):
120
217
  """
121
- Updates columns in this row.
218
+ Updates columns in this row (write-through to DB + local cache).
122
219
  """
123
220
  data = {}
124
221
  if dict_:
@@ -126,33 +223,12 @@ class Row:
126
223
  if kwds:
127
224
  data.update(kwds)
128
225
  if data:
129
- if hasattr(self.table, "updins"):
130
- self.table.updins(data, pk=self.pk)
131
- elif hasattr(self.table, "upsert"):
132
- self.table.upsert(data, pk=self.pk)
133
- else:
134
- self.table.update(data, pk=self.pk)
226
+ self.table.update_or_insert(data, pk=self.pk)
227
+ if self._cache is not None:
228
+ self._cache.update(data)
229
+ object.__setattr__(self, "_column_set", None)
135
230
  return self
136
231
 
137
- def __cmp__(self, other):
138
- """
139
- Legacy comparison method; returns 0 if self and other share keys/values, else -1.
140
- """
141
- diff = -1
142
- if hasattr(other, "keys"):
143
- k1 = list(self.keys())
144
- k2 = list(other.keys())
145
- if k1 == k2:
146
- diff = 0
147
- for k in k1:
148
- if self[k] != other[k]:
149
- diff = -1
150
- break
151
- return diff
152
-
153
- def __bool__(self):
154
- return bool(len(self))
155
-
156
232
  def copy(self, lock=None):
157
233
  """
158
234
  Makes a copy of this row with a new sys_id, dropping sys_-prefixed columns from the new dict.
@@ -163,7 +239,7 @@ class Row:
163
239
  old.pop(k)
164
240
  return self.table.new(old, lock=lock)
165
241
 
166
- def pop(self):
242
+ def pop(self, key, *args):
167
243
  raise NotImplementedError
168
244
 
169
245
  def popitem(self):
@@ -187,9 +263,9 @@ class Row:
187
263
 
188
264
  def to_dict(self):
189
265
  """
190
- Returns the row as a dictionary via a SELECT on self.pk.
266
+ Returns the row as a dictionary (from cache or via a SELECT on self.pk).
191
267
  """
192
- return self.table.select(where=self.pk).as_dict().one()
268
+ return dict(self._ensure_cache())
193
269
 
194
270
  def extract(self, *args):
195
271
  """
@@ -1,6 +1,3 @@
1
- import psycopg2
2
-
3
-
4
1
  class Sequence:
5
2
  """
6
3
  Represents a database sequence in PostgreSQL.
@@ -48,8 +45,12 @@ class Sequence:
48
45
  sql = f"SELECT currval('{self.name}');"
49
46
  try:
50
47
  return self.tx.execute(sql, ()).scalar()
51
- except psycopg2.ProgrammingError:
52
- return None
48
+ except Exception as e:
49
+ # Catch psycopg2.ProgrammingError (or equivalent) when no
50
+ # value has been generated in this session.
51
+ if "currval" in str(e).lower():
52
+ return None
53
+ raise
53
54
 
54
55
  def set_value(self, start=None):
55
56
  """
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import warnings
2
3
  import sqlparse
3
4
  from collections.abc import Iterable, Mapping
4
5
  from velocity.db import exceptions
@@ -597,9 +598,9 @@ class Table:
597
598
 
598
599
  def foreign_keys(self):
599
600
  """
600
- Returns the list of foreign key columns for this table (may be incomplete).
601
+ Returns the list of foreign key columns for this table.
601
602
  """
602
- sql, vals = self.sql.primary_keys(self.name)
603
+ sql, vals = self.sql.foreign_keys(self.name)
603
604
  result = self.tx.execute(sql, vals, cursor=self.cursor())
604
605
  return [x[0] for x in result.as_tuple()]
605
606
 
@@ -918,8 +919,8 @@ class Table:
918
919
  else:
919
920
  exists_where = pk
920
921
 
921
- ins_builder = getattr(self.sql, "insnx", None) or getattr(
922
- self.sql, "insert_if_not_exists", None
922
+ ins_builder = getattr(self.sql, "insert_if_not_exists", None) or getattr(
923
+ self.sql, "insnx", None
923
924
  )
924
925
  if ins_builder is None:
925
926
  raise NotImplementedError(
@@ -932,7 +933,14 @@ class Table:
932
933
  result = self.tx.execute(sql, vals, cursor=self.cursor())
933
934
  return result.cursor.rowcount if result.cursor else 0
934
935
 
935
- updins = update_or_insert
936
+ @property
937
+ def updins(self):
938
+ warnings.warn(
939
+ "Table.updins is deprecated, use Table.update_or_insert instead.",
940
+ DeprecationWarning,
941
+ stacklevel=2,
942
+ )
943
+ return self.update_or_insert
936
944
 
937
945
  @create_missing
938
946
  def insert_if_not_exists(self, data, where=None, **kwds):
@@ -953,10 +961,25 @@ class Table:
953
961
  result = self.tx.execute(sql, vals, cursor=self.cursor())
954
962
  return result.cursor.rowcount if result.cursor else 0
955
963
 
956
- insnx = insert_if_not_exists
964
+ @property
965
+ def insnx(self):
966
+ warnings.warn(
967
+ "Table.insnx is deprecated, use Table.insert_if_not_exists instead.",
968
+ DeprecationWarning,
969
+ stacklevel=2,
970
+ )
971
+ return self.insert_if_not_exists
957
972
 
958
973
  upsert = merge
959
- indate = merge
974
+
975
+ @property
976
+ def indate(self):
977
+ warnings.warn(
978
+ "Table.indate is deprecated, use Table.merge (or Table.upsert) instead.",
979
+ DeprecationWarning,
980
+ stacklevel=2,
981
+ )
982
+ return self.merge
960
983
 
961
984
  @return_default(0)
962
985
  def count(self, where=None, **kwds):
@@ -1167,37 +1190,17 @@ class Table:
1167
1190
  def duplicate_rows(self, columns=None, where=None, orderby=None, **kwds):
1168
1191
  """
1169
1192
  Returns rows that have duplicates in the specified `columns`.
1170
- TBD: Move code to generate sql to the sql module. it should not be
1171
- here so different sql engines can use this function.
1172
1193
  """
1173
1194
  if not columns:
1174
1195
  raise ValueError(
1175
1196
  "You must specify at least one column to check for duplicates."
1176
1197
  )
1177
- sql, vals = self.sql.select(
1178
- self.tx,
1179
- columns=columns,
1180
- table=self.name,
1181
- where=where,
1182
- groupby=columns,
1183
- having={">count(*)": 1},
1198
+ sql, vals = self.sql.duplicate_rows(
1199
+ self.tx, table=self.name, columns=columns, where=where, orderby=orderby
1184
1200
  )
1185
- if orderby:
1186
- orderby = [orderby] if isinstance(orderby, str) else orderby
1187
- else:
1188
- orderby = columns
1189
- subjoin = " AND ".join([f"t.{col} = dup.{col}" for col in columns])
1190
- ob = ", ".join(orderby)
1191
- final_sql = f"""
1192
- SELECT t.*
1193
- FROM {self.name} t
1194
- JOIN ({sql}) dup
1195
- ON {subjoin}
1196
- ORDER BY {ob}
1197
- """
1198
1201
  if kwds.get("sql_only", False):
1199
- return final_sql, vals
1200
- return self.tx.execute(final_sql, vals)
1202
+ return sql, vals
1203
+ return self.tx.execute(sql, vals)
1201
1204
 
1202
1205
  def has_duplicates(self, columns=None, where=None, **kwds):
1203
1206
  """
@@ -1290,12 +1293,10 @@ class Table:
1290
1293
  def lock(self, mode="ACCESS EXCLUSIVE", wait_for_lock=None, **kwds):
1291
1294
  """
1292
1295
  Issues a LOCK TABLE statement for this table.
1293
- TBD: MOve SQL To sql module so we can use this function with other engines.
1294
1296
  """
1295
- sql = f"LOCK TABLE {self.name} IN {mode} MODE"
1296
- if not wait_for_lock:
1297
- sql += " NOWAIT"
1298
- vals = None
1297
+ sql, vals = self.sql.lock_table(
1298
+ table=self.name, mode=mode, wait_for_lock=wait_for_lock
1299
+ )
1299
1300
  if kwds.get("sql_only", False):
1300
1301
  return sql, vals
1301
1302
  return self.tx.execute(sql, vals)
@@ -1468,62 +1469,14 @@ class Table:
1468
1469
  def find_duplicates(self, columns, sql_only=False):
1469
1470
  """
1470
1471
  Returns duplicate groups from the table based on the specified columns in a case-insensitive way.
1471
-
1472
- For each column, the subquery computes:
1473
- - lower(column) AS normalized_<column>
1474
- - array_agg(column) AS variations_<column>
1475
- - array_agg(sys_id) AS sys_ids
1476
- - COUNT(*) AS total_count
1477
-
1478
- The subquery groups by lower(column) values and retains only groups with more than one row.
1479
-
1480
- Example SQL for a single column "email_address":
1481
-
1482
- SELECT
1483
- lower(email_address) AS normalized_email_address,
1484
- array_agg(email_address) AS variations_email_address,
1485
- array_agg(sys_id) AS sys_ids,
1486
- COUNT(*) AS total_count
1487
- FROM donor_users
1488
- GROUP BY lower(email_address)
1489
- HAVING COUNT(*) > 1
1490
-
1491
- Parameters:
1492
- columns (list or str): Column name or list of column names to check duplicates on.
1493
- sql_only (bool): If True, returns the SQL string and an empty tuple; otherwise,
1494
- executes the query using self.tx.execute.
1495
-
1496
- Returns:
1497
- A tuple of (SQL string, parameters) if sql_only is True, otherwise the result of executing the query.
1498
1472
  """
1499
1473
  if not columns:
1500
1474
  raise ValueError(
1501
1475
  "You must specify at least one column to check for duplicates."
1502
1476
  )
1503
-
1504
1477
  if isinstance(columns, str):
1505
1478
  columns = [columns]
1506
-
1507
- # Build subquery SELECT clause parts for normalized values and variations.
1508
- normalized_cols = [f"lower({col}) AS normalized_{col}" for col in columns]
1509
- variations_cols = [f"array_agg({col}) AS variations_{col}" for col in columns]
1510
-
1511
- subquery_select = ",\n ".join(
1512
- normalized_cols
1513
- + variations_cols
1514
- + ["array_agg(sys_id) AS sys_ids", "COUNT(*) AS total_count"]
1515
- )
1516
-
1517
- groupby_clause = ", ".join([f"lower({col})" for col in columns])
1518
-
1519
- subquery = f"""
1520
- SELECT
1521
- {subquery_select}
1522
- FROM {self.name}
1523
- GROUP BY {groupby_clause}
1524
- HAVING COUNT(*) > 1
1525
- """
1526
-
1479
+ sql, vals = self.sql.find_duplicates(table=self.name, columns=columns)
1527
1480
  if sql_only:
1528
- return subquery, ()
1529
- return self.tx.execute(subquery, ())
1481
+ return sql, vals
1482
+ return self.tx.execute(sql, vals)
@@ -530,3 +530,51 @@ class BaseSQLDialect(ABC):
530
530
  Tuple of (sql_string, parameters)
531
531
  """
532
532
  pass
533
+
534
+ # ------------------------------------------------------------------
535
+ # Optional methods — dialects may override if they support these
536
+ # ------------------------------------------------------------------
537
+
538
+ @classmethod
539
+ def foreign_keys(cls, table: str) -> Tuple[str, tuple]:
540
+ """Return foreign key column names for the given table."""
541
+ raise NotImplementedError(
542
+ f"{cls.server} dialect does not implement foreign_keys()"
543
+ )
544
+
545
+ @classmethod
546
+ def lock_table(
547
+ cls,
548
+ table: str,
549
+ mode: str = "ACCESS EXCLUSIVE",
550
+ wait_for_lock: Optional[bool] = None,
551
+ ) -> Tuple[str, tuple]:
552
+ """Generate a LOCK TABLE statement."""
553
+ raise NotImplementedError(
554
+ f"{cls.server} dialect does not implement lock_table()"
555
+ )
556
+
557
+ @classmethod
558
+ def duplicate_rows(
559
+ cls,
560
+ tx: Any,
561
+ table: str,
562
+ columns: List[str],
563
+ where: Optional[Union[str, Dict, List]] = None,
564
+ orderby: Optional[Union[str, List[str]]] = None,
565
+ ) -> Tuple[str, tuple]:
566
+ """Return rows that have duplicates in the specified columns."""
567
+ raise NotImplementedError(
568
+ f"{cls.server} dialect does not implement duplicate_rows()"
569
+ )
570
+
571
+ @classmethod
572
+ def find_duplicates(
573
+ cls,
574
+ table: str,
575
+ columns: List[str],
576
+ ) -> Tuple[str, tuple]:
577
+ """Return duplicate groups with case-insensitive matching."""
578
+ raise NotImplementedError(
579
+ f"{cls.server} dialect does not implement find_duplicates()"
580
+ )