velocity-python 0.0.175__tar.gz → 0.0.176__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 (139) hide show
  1. {velocity_python-0.0.175 → velocity_python-0.0.176}/PKG-INFO +1 -1
  2. {velocity_python-0.0.175 → velocity_python-0.0.176}/pyproject.toml +1 -1
  3. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.176/src/velocity/aws/handlers/mixins/__init__.py +12 -0
  5. velocity_python-0.0.176/src/velocity/aws/handlers/mixins/data_service.py +530 -0
  6. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity_python.egg-info/PKG-INFO +1 -1
  7. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity_python.egg-info/SOURCES.txt +1 -0
  8. velocity_python-0.0.175/src/velocity/aws/handlers/mixins/__init__.py +0 -10
  9. {velocity_python-0.0.175 → velocity_python-0.0.176}/LICENSE +0 -0
  10. {velocity_python-0.0.175 → velocity_python-0.0.176}/README.md +0 -0
  11. {velocity_python-0.0.175 → velocity_python-0.0.176}/setup.cfg +0 -0
  12. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/invoices.py +0 -0
  14. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/orders.py +0 -0
  15. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/payments.py +0 -0
  16. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/purchase_orders.py +0 -0
  17. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/tests/__init__.py +0 -0
  18. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/tests/test_email_processing.py +0 -0
  19. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  20. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  21. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/__init__.py +0 -0
  24. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/base_handler.py +0 -0
  25. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/context.py +0 -0
  26. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/exceptions.py +0 -0
  27. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  28. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  29. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/response.py +0 -0
  30. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  31. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/tests/__init__.py +0 -0
  32. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  33. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/aws/tests/test_response.py +0 -0
  34. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/__init__.py +0 -0
  35. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/__init__.py +0 -0
  36. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/column.py +0 -0
  37. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/database.py +0 -0
  38. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/decorators.py +0 -0
  39. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/engine.py +0 -0
  40. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/result.py +0 -0
  41. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/row.py +0 -0
  42. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/sequence.py +0 -0
  43. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/table.py +0 -0
  44. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/core/transaction.py +0 -0
  45. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/exceptions.py +0 -0
  46. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/__init__.py +0 -0
  47. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/base/__init__.py +0 -0
  48. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/base/initializer.py +0 -0
  49. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/base/operators.py +0 -0
  50. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/base/sql.py +0 -0
  51. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/base/types.py +0 -0
  52. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/mysql/__init__.py +0 -0
  53. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/mysql/operators.py +0 -0
  54. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/mysql/reserved.py +0 -0
  55. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/mysql/sql.py +0 -0
  56. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/mysql/types.py +0 -0
  57. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/postgres/__init__.py +0 -0
  58. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/postgres/operators.py +0 -0
  59. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/postgres/reserved.py +0 -0
  60. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/postgres/sql.py +0 -0
  61. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/postgres/types.py +0 -0
  62. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  63. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlite/operators.py +0 -0
  64. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  65. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlite/sql.py +0 -0
  66. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlite/types.py +0 -0
  67. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  68. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  69. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  70. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  71. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/sqlserver/types.py +0 -0
  72. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/servers/tablehelper.py +0 -0
  73. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/__init__.py +0 -0
  74. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/common_db_test.py +0 -0
  75. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/__init__.py +0 -0
  76. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/common.py +0 -0
  77. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_column.py +0 -0
  78. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  79. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_database.py +0 -0
  80. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  81. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  82. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  83. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_result.py +0 -0
  84. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_row.py +0 -0
  85. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  86. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  87. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  88. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  89. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  90. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_table.py +0 -0
  91. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  92. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  93. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/sql/__init__.py +0 -0
  94. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/sql/common.py +0 -0
  95. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  96. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  97. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  98. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_db_utils.py +0 -0
  99. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_postgres.py +0 -0
  100. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  101. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  102. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_result_caching.py +0 -0
  103. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  104. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  105. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  106. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  107. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_sql_builder.py +0 -0
  108. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/tests/test_tablehelper.py +0 -0
  109. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/db/utils.py +0 -0
  110. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/logging.py +0 -0
  111. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/__init__.py +0 -0
  112. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/conv/__init__.py +0 -0
  113. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/conv/iconv.py +0 -0
  114. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/conv/oconv.py +0 -0
  115. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/db.py +0 -0
  116. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/export.py +0 -0
  117. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/format.py +0 -0
  118. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/mail.py +0 -0
  119. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/merge.py +0 -0
  120. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/__init__.py +0 -0
  121. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_db.py +0 -0
  122. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_fix.py +0 -0
  123. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_format.py +0 -0
  124. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_iconv.py +0 -0
  125. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_merge.py +0 -0
  126. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_oconv.py +0 -0
  127. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_original_error.py +0 -0
  128. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tests/test_timer.py +0 -0
  129. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/timer.py +0 -0
  130. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity/misc/tools.py +0 -0
  131. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  132. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity_python.egg-info/requires.txt +0 -0
  133. {velocity_python-0.0.175 → velocity_python-0.0.176}/src/velocity_python.egg-info/top_level.txt +0 -0
  134. {velocity_python-0.0.175 → velocity_python-0.0.176}/tests/test_decorators.py +0 -0
  135. {velocity_python-0.0.175 → velocity_python-0.0.176}/tests/test_lambda_handler.py +0 -0
  136. {velocity_python-0.0.175 → velocity_python-0.0.176}/tests/test_mixins_import.py +0 -0
  137. {velocity_python-0.0.175 → velocity_python-0.0.176}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  138. {velocity_python-0.0.175 → velocity_python-0.0.176}/tests/test_table_alter.py +0 -0
  139. {velocity_python-0.0.175 → velocity_python-0.0.176}/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.175
3
+ Version: 0.0.176
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.175"
7
+ version = "0.0.176"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.175"
1
+ __version__ = version = "0.0.176"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1,12 @@
1
+ """
2
+ Mixins for AWS Lambda handlers.
3
+
4
+ This package provides mixins for common Lambda handler patterns:
5
+ - WebHandler: Activity tracking, logging, and error handling
6
+ - DataServiceMixin: Generic CRUD operations for database-backed APIs
7
+ """
8
+
9
+ from .web_handler import WebHandler
10
+ from .data_service import DataServiceMixin
11
+
12
+ __all__ = ['WebHandler', 'DataServiceMixin']
@@ -0,0 +1,530 @@
1
+ """
2
+ DataServiceMixin - Generic CRUD operations for Lambda handlers.
3
+
4
+ Provides standard database operations that can be mixed into any Lambda handler
5
+ that uses velocity.db for database access.
6
+ """
7
+
8
+ import base64
9
+ import datetime
10
+ from io import BytesIO
11
+
12
+ from velocity.misc import export
13
+
14
+
15
+ class DataServiceMixin:
16
+ """
17
+ Mixin providing generic CRUD operations for Lambda handlers.
18
+
19
+ This mixin assumes:
20
+ - Handler uses velocity.db engine with @engine.transaction decorator
21
+ - Handler has a context object with payload() and response() methods
22
+ - Database tables follow standard conventions (sys_id primary key)
23
+
24
+ Usage:
25
+ from velocity.aws.handlers.mixins import DataServiceMixin
26
+
27
+ @engine.transaction
28
+ class HttpEventHandler(DataServiceMixin, LambdaHandler):
29
+ def __init__(self, aws_event, aws_context):
30
+ super().__init__(aws_event, aws_context)
31
+
32
+ Override read_hook, write_hook, etc. methods to add custom business logic.
33
+ """
34
+
35
+ # PostgreSQL type mappings for frontend display
36
+ _pg_types = {
37
+ "bool": "string",
38
+ "char": "string",
39
+ "int2": "string",
40
+ "int4": "string",
41
+ "int8": "string",
42
+ "text": "string",
43
+ "numeric": "number",
44
+ "float4": "number",
45
+ "float8": "number",
46
+ "varchar": "string",
47
+ "date": "string",
48
+ "time": "string",
49
+ "timestamp": "string",
50
+ }
51
+
52
+ @classmethod
53
+ def _get_field_type(cls, column_info):
54
+ """Convert database column type to frontend display type"""
55
+ return (
56
+ "string"
57
+ if column_info["name"] in ["id", "sys_id"]
58
+ else cls._pg_types.get(column_info["type_name"], "string")
59
+ )
60
+
61
+ # ========== Hook Methods (Override These) ==========
62
+
63
+ def read_hook(self, tx, table_name, sys_id, row, context):
64
+ """
65
+ Called after reading an object. Override to add custom logic.
66
+
67
+ Args:
68
+ tx: Database transaction
69
+ table_name: Name of the table
70
+ sys_id: Record ID
71
+ row: Dictionary of record data
72
+ context: Request context
73
+ """
74
+ pass
75
+
76
+ def write_hook(self, tx, table_name, sys_id, incoming, context):
77
+ """
78
+ Called before writing an object. Override to add validation/transformation.
79
+
80
+ Args:
81
+ tx: Database transaction
82
+ table_name: Name of the table
83
+ sys_id: Record ID (or "@new" for new records)
84
+ incoming: Dictionary of data to write
85
+ context: Request context
86
+ """
87
+ pass
88
+
89
+ def query_hook(self, tx, table_name, params, data, context):
90
+ """
91
+ Called after querying objects. Override to transform results.
92
+
93
+ Args:
94
+ tx: Database transaction
95
+ table_name: Name of the table
96
+ params: Query parameters
97
+ data: Query results
98
+ context: Request context
99
+ """
100
+ pass
101
+
102
+ def delete_hook(self, tx, table_name, sys_id, context):
103
+ """
104
+ Called before deleting an object. Override to add authorization.
105
+
106
+ Args:
107
+ tx: Database transaction
108
+ table_name: Name of the table
109
+ sys_id: Record ID to delete
110
+ context: Request context
111
+ """
112
+ pass
113
+
114
+ # ========== CRUD Action Methods ==========
115
+
116
+ def OnActionReadObject(self, tx, context):
117
+ """
118
+ Read a single object by sys_id.
119
+
120
+ Payload:
121
+ tableName: str - Name of the database table
122
+ object: dict - Object containing sys_id field
123
+ """
124
+ payload = context.payload()
125
+
126
+ # Validate required parameters
127
+ if "tableName" not in payload:
128
+ raise ValueError("Missing required parameter 'tableName' in payload")
129
+ if "object" not in payload:
130
+ raise ValueError("Missing required parameter 'object' in payload")
131
+
132
+ table_name = payload["tableName"]
133
+ obj = payload["object"]
134
+
135
+ if not table_name:
136
+ raise ValueError("Parameter 'tableName' cannot be empty")
137
+ if not obj:
138
+ raise ValueError("Parameter 'object' cannot be empty")
139
+
140
+ sys_id = obj.get("sys_id")
141
+
142
+ if not sys_id:
143
+ raise ValueError("Object must contain 'sys_id' field")
144
+
145
+ if sys_id == "@new":
146
+ row = {}
147
+ else:
148
+ sys_id = int(sys_id)
149
+ row = tx.table(table_name).find(sys_id)
150
+ row = row.to_dict() if row else {}
151
+
152
+ # Call hook for custom logic
153
+ self.read_hook(tx, table_name, sys_id, row, context)
154
+
155
+ context.response().set_body({
156
+ "object": row,
157
+ "lastFetch": datetime.datetime.now(),
158
+ })
159
+
160
+ if row:
161
+ context.response().load_object(row)
162
+ else:
163
+ message = f"Object {obj.get('sys_id')} was not found in the database. You may create it as a new object."
164
+ context.response().toast(message, "warning")
165
+
166
+ def OnActionFindObject(self, tx, context):
167
+ """
168
+ Find a single object by query predicate.
169
+
170
+ Payload:
171
+ tableName: str - Name of the database table
172
+ query: dict - Query containing 'where' clause
173
+ """
174
+ payload = context.payload()
175
+
176
+ # Validate required parameters
177
+ if "tableName" not in payload:
178
+ raise ValueError("Missing required parameter 'tableName' in payload")
179
+ if "query" not in payload:
180
+ raise ValueError("Missing required parameter 'query' in payload")
181
+
182
+ table_name = payload["tableName"]
183
+ query = payload["query"]
184
+
185
+ if not table_name:
186
+ raise ValueError("Parameter 'tableName' cannot be empty")
187
+ if not query or "where" not in query:
188
+ raise ValueError("Parameter 'query' must contain 'where' clause")
189
+
190
+ row = tx.table(table_name).find(query["where"])
191
+
192
+ if row:
193
+ row = row.to_dict()
194
+ self.read_hook(tx, table_name, None, row, context)
195
+ else:
196
+ row = {}
197
+
198
+ context.response().set_body({
199
+ "object": row,
200
+ "lastFetch": datetime.datetime.now(),
201
+ })
202
+ context.response().load_object(row)
203
+
204
+ def OnActionWriteObject(self, tx, context):
205
+ """
206
+ Write (insert or update) an object.
207
+
208
+ Payload:
209
+ tableName: str - Name of the database table
210
+ object: dict - Object data to write
211
+ """
212
+ payload = context.payload()
213
+
214
+ # Validate required parameters
215
+ if "tableName" not in payload:
216
+ raise ValueError("Missing required parameter 'tableName' in payload")
217
+ if "object" not in payload:
218
+ raise ValueError("Missing required parameter 'object' in payload")
219
+
220
+ table_name = payload["tableName"]
221
+ obj = payload["object"]
222
+
223
+ if not table_name:
224
+ raise ValueError("Parameter 'tableName' cannot be empty")
225
+ if not obj or not isinstance(obj, dict):
226
+ raise ValueError("Parameter 'object' must be a non-empty dictionary")
227
+
228
+ # Ensure the object has at least some data
229
+ incoming = obj.copy()
230
+ if not any(value is not None for value in incoming.values()):
231
+ raise ValueError("Parameter 'object' cannot contain only None values")
232
+
233
+ sys_id = incoming.pop("sys_id", None)
234
+
235
+ # Call hook before write
236
+ self.write_hook(tx, table_name, sys_id, incoming, context)
237
+
238
+ try:
239
+ if sys_id == "@new":
240
+ row = tx.table(table_name).new()
241
+ row.update(incoming)
242
+ elif sys_id:
243
+ sys_id = int(sys_id)
244
+ row = tx.table(table_name).get(sys_id)
245
+ row.update(incoming)
246
+ else:
247
+ raise ValueError("Object sys_id was not supplied on write operation")
248
+
249
+ row_dict = row.to_dict()
250
+
251
+ context.response().set_body({
252
+ "object": row_dict,
253
+ "lastFetch": datetime.datetime.now(),
254
+ })
255
+ context.response().load_object(row_dict)
256
+
257
+ except Exception as e:
258
+ raise Exception(f"Failed to write object to {table_name}: {str(e)}")
259
+
260
+ def OnActionQuery(self, tx, context):
261
+ """
262
+ Query table for multiple rows.
263
+
264
+ Payload:
265
+ obj: str - Table name to query
266
+ params: dict - Query parameters (where, orderby, limit, offset)
267
+ result_format: str - 'excel', 'raw', or 'datatable' (default)
268
+ datatable: str - Name for datatable in response (defaults to obj)
269
+ count: bool - Include total count in response
270
+ headers: bool - Include column headers in response
271
+ """
272
+ payload = context.payload()
273
+
274
+ # Validate required parameters
275
+ if "obj" not in payload:
276
+ raise ValueError("Missing required parameter 'obj' in payload")
277
+
278
+ table = payload["obj"]
279
+ if not table:
280
+ raise ValueError("Parameter 'obj' cannot be empty")
281
+
282
+ params = payload.get("params", {})
283
+ result = tx.table(table).select(**params)
284
+
285
+ if payload.get("result_format") == "excel":
286
+ headers = payload.get(
287
+ "headers", [x.replace("_", " ").title() for x in result.headers]
288
+ )
289
+ rows = result.as_list().all()
290
+
291
+ filebuffer = BytesIO()
292
+ export.create_spreadsheet(headers, rows, filebuffer)
293
+ context.response().file_download({
294
+ "filename": payload.get("filename", "temp_file.xls"),
295
+ "data": base64.b64encode(filebuffer.getvalue()).decode(),
296
+ })
297
+ return
298
+
299
+ data = {
300
+ "rows": result.all(),
301
+ "config": {
302
+ "lastFetch": datetime.datetime.now(),
303
+ "query": result.sql,
304
+ "format": payload.get("result_format"),
305
+ },
306
+ }
307
+
308
+ if payload.get("count"):
309
+ data["count"] = tx.table(table).count(where=params.get("where", None))
310
+
311
+ if payload.get("headers"):
312
+ data["columns"] = [
313
+ {
314
+ "field": x["name"],
315
+ "headerName": x["name"].replace("_", " ").title(),
316
+ "type": self._get_field_type(x),
317
+ }
318
+ for x in result.columns.values()
319
+ ]
320
+
321
+ # Call hook after query
322
+ self.query_hook(tx, table, params, data, context)
323
+
324
+ if payload.get("result_format") == "raw":
325
+ context.response().set_body(data)
326
+ else:
327
+ context.response().set_table({
328
+ payload.get("datatable", payload.get("obj")): data
329
+ })
330
+
331
+ def OnActionDeleteObject(self, tx, context):
332
+ """
333
+ Delete one or more objects.
334
+
335
+ Payload:
336
+ tableName: str - Name of the database table
337
+ deleteList: list - List of sys_id values to delete (optional)
338
+ object: dict - Single object with sys_id to delete (optional)
339
+ """
340
+ payload = context.payload()
341
+
342
+ # Validate required parameters
343
+ if "tableName" not in payload:
344
+ raise ValueError("Missing required parameter 'tableName' in payload")
345
+
346
+ table_name = payload["tableName"]
347
+ if not table_name:
348
+ raise ValueError("Parameter 'tableName' cannot be empty")
349
+
350
+ table = tx.table(table_name)
351
+ deleteList = []
352
+
353
+ if "deleteList" in payload:
354
+ deleteList.extend(payload.get("deleteList"))
355
+
356
+ if "object" in payload:
357
+ obj = payload["object"]
358
+ if obj and obj.get("sys_id"):
359
+ deleteList.append(obj.get("sys_id"))
360
+
361
+ for sys_id in deleteList:
362
+ # Call hook before delete
363
+ self.delete_hook(tx, table_name, sys_id, context)
364
+
365
+ obj = table.find(int(sys_id))
366
+ if obj:
367
+ obj.clear()
368
+ context.response().toast(f"Object {sys_id} deleted", "success")
369
+ else:
370
+ context.response().toast(f"Object {sys_id} not found", "warning")
371
+
372
+ if not deleteList:
373
+ context.response().toast("No items were selected.", "warning")
374
+
375
+ def OnActionGetTables(self, tx, context):
376
+ """Get list of all tables in the database."""
377
+ context.response().set_body({"tables": tx.tables()})
378
+
379
+ def OnActionUpdateRows(self, tx, context):
380
+ """
381
+ Update multiple rows with the same data.
382
+
383
+ Payload:
384
+ table: str - Table name
385
+ updateData: dict - Data to update
386
+ updateRows: list - List of sys_id values to update
387
+ """
388
+ payload = context.payload()
389
+
390
+ # Validate required parameters
391
+ required_fields = ["updateData", "updateRows", "table"]
392
+ for field in required_fields:
393
+ if field not in payload:
394
+ raise ValueError(f"Missing required parameter '{field}' in payload")
395
+
396
+ data = payload["updateData"]
397
+ rows = payload["updateRows"]
398
+ table = payload["table"]
399
+
400
+ if not table:
401
+ raise ValueError("Parameter 'table' cannot be empty")
402
+ if not rows:
403
+ raise ValueError("Parameter 'updateRows' cannot be empty")
404
+
405
+ t = tx.table(table)
406
+ count = t.update(data, {"sys_id": rows})
407
+ context.response().toast(f"Updated {count} item(s).", "success")
408
+
409
+ def OnActionQueryDirect(self, tx, context):
410
+ """
411
+ Query table directly with velocity.db parameters.
412
+
413
+ Payload:
414
+ obj: str - Table name
415
+ params: dict - Direct velocity.db select parameters
416
+ result_format: str - 'excel', 'raw', or 'datatable' (default)
417
+ count: bool - Include total count
418
+ headers: bool - Include column metadata
419
+ """
420
+ payload = context.payload()
421
+
422
+ # Validate required parameters
423
+ if "obj" not in payload:
424
+ raise ValueError("Missing required parameter 'obj' in payload")
425
+
426
+ table_name = payload["obj"]
427
+ if not table_name:
428
+ raise ValueError("Parameter 'obj' cannot be empty")
429
+
430
+ params = payload.get("params", {})
431
+
432
+ if payload.get("result_format") == "excel":
433
+ result = tx.table(table_name).select(**params)
434
+ headers = payload.get(
435
+ "headers", [x.replace("_", " ").title() for x in result.headers]
436
+ )
437
+ rows = result.as_list().all()
438
+ filebuffer = BytesIO()
439
+ export.create_spreadsheet(headers, rows, filebuffer)
440
+ context.response().file_download({
441
+ "filename": payload.get("filename", "temp_file.xls"),
442
+ "data": base64.b64encode(filebuffer.getvalue()).decode(),
443
+ })
444
+ return
445
+
446
+ result = tx.table(table_name).select(**params)
447
+ data = {
448
+ "rows": result.all(),
449
+ "config": {
450
+ "lastFetch": datetime.datetime.now(),
451
+ "query": result.sql,
452
+ "format": payload.get("result_format"),
453
+ },
454
+ }
455
+
456
+ if payload.get("count"):
457
+ data["count"] = tx.table(table_name).count(where=params.get("where", None))
458
+
459
+ if payload.get("headers"):
460
+ data["columns"] = [
461
+ {
462
+ "field": x["name"],
463
+ "headerName": x["name"].replace("_", " ").title(),
464
+ "type": self._get_field_type(x),
465
+ }
466
+ for x in result.columns.values()
467
+ ]
468
+
469
+ if payload.get("result_format") == "raw":
470
+ context.response().set_body(data)
471
+ else:
472
+ context.response().set_table({
473
+ payload.get("datatable", payload.get("obj")): data
474
+ })
475
+
476
+ def OnActionGetTableSchema(self, tx, context):
477
+ """
478
+ Get table schema from information_schema.
479
+
480
+ Payload:
481
+ tableName: str - Name of the table
482
+ """
483
+ payload = context.payload()
484
+
485
+ # Validate required parameters
486
+ if "tableName" not in payload:
487
+ raise ValueError("Missing required parameter 'tableName' in payload")
488
+
489
+ table_name = payload["tableName"]
490
+ if not table_name:
491
+ raise ValueError("Parameter 'tableName' cannot be empty")
492
+
493
+ try:
494
+ # Query information_schema to get table schema
495
+ schema_query = """
496
+ SELECT
497
+ column_name,
498
+ data_type,
499
+ is_nullable,
500
+ column_default,
501
+ character_maximum_length,
502
+ numeric_precision,
503
+ numeric_scale,
504
+ ordinal_position
505
+ FROM information_schema.columns
506
+ WHERE table_name = %s
507
+ AND table_schema = 'public'
508
+ ORDER BY ordinal_position
509
+ """
510
+
511
+ schema_data = tx.execute(schema_query, [table_name])
512
+
513
+ if not schema_data:
514
+ raise ValueError(
515
+ f"Table '{table_name}' not found or has no accessible columns"
516
+ )
517
+
518
+ context.response().set_table({
519
+ table_name: {
520
+ "schema": schema_data.all(),
521
+ }
522
+ })
523
+
524
+ except Exception as e:
525
+ if hasattr(self, 'log'):
526
+ self.log(
527
+ f"Error retrieving schema for table {table_name}: {str(e)}",
528
+ "OnActionGetTableSchema",
529
+ )
530
+ raise Exception(f"Failed to retrieve table schema: {str(e)}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.175
3
+ Version: 0.0.176
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
@@ -22,6 +22,7 @@ src/velocity/aws/handlers/lambda_handler.py
22
22
  src/velocity/aws/handlers/response.py
23
23
  src/velocity/aws/handlers/sqs_handler.py
24
24
  src/velocity/aws/handlers/mixins/__init__.py
25
+ src/velocity/aws/handlers/mixins/data_service.py
25
26
  src/velocity/aws/handlers/mixins/web_handler.py
26
27
  src/velocity/aws/tests/__init__.py
27
28
  src/velocity/aws/tests/test_lambda_handler_json_serialization.py
@@ -1,10 +0,0 @@
1
- """
2
- Mixins for AWS Lambda handlers.
3
-
4
- This package exposes a single WebHandler mixin that unifies activity tracking,
5
- logging, and error handling helpers for Lambda handlers.
6
- """
7
-
8
- from .web_handler import WebHandler
9
-
10
- __all__ = ['WebHandler']