velocity-python 0.1.17__tar.gz → 0.1.20__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 (172) hide show
  1. {velocity_python-0.1.17/src/velocity_python.egg-info → velocity_python-0.1.20}/PKG-INFO +2 -2
  2. {velocity_python-0.1.17 → velocity_python-0.1.20}/pyproject.toml +2 -2
  3. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/base_handler.py +10 -9
  5. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/exceptions.py +6 -1
  6. velocity_python-0.1.20/src/velocity/misc/pdf.py +217 -0
  7. {velocity_python-0.1.17 → velocity_python-0.1.20/src/velocity_python.egg-info}/PKG-INFO +2 -2
  8. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/requires.txt +1 -1
  9. velocity_python-0.1.20/tests/test_pdf.py +154 -0
  10. velocity_python-0.1.17/src/velocity/misc/pdf.py +0 -201
  11. velocity_python-0.1.17/tests/test_pdf.py +0 -188
  12. {velocity_python-0.1.17 → velocity_python-0.1.20}/LICENSE +0 -0
  13. {velocity_python-0.1.17 → velocity_python-0.1.20}/README.md +0 -0
  14. {velocity_python-0.1.17 → velocity_python-0.1.20}/setup.cfg +0 -0
  15. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/__init__.py +0 -0
  16. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/amplify.py +0 -0
  17. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/amplify_build.py +0 -0
  18. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/__init__.py +0 -0
  19. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/context.py +0 -0
  20. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/context_factory.py +0 -0
  21. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  22. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  23. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  24. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  25. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/perf.py +0 -0
  26. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/response.py +0 -0
  27. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  28. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/__init__.py +0 -0
  29. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  30. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  31. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_response.py +0 -0
  32. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/__init__.py +0 -0
  33. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/__init__.py +0 -0
  34. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/async_support.py +0 -0
  35. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/column.py +0 -0
  36. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/database.py +0 -0
  37. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/decorators.py +0 -0
  38. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/engine.py +0 -0
  39. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/result.py +0 -0
  40. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/row.py +0 -0
  41. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/sequence.py +0 -0
  42. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/table.py +0 -0
  43. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/transaction.py +0 -0
  44. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/view.py +0 -0
  45. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/exceptions.py +0 -0
  46. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/migrations.py +0 -0
  47. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/__init__.py +0 -0
  48. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/__init__.py +0 -0
  49. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/initializer.py +0 -0
  50. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/operators.py +0 -0
  51. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/sql.py +0 -0
  52. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/types.py +0 -0
  53. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/__init__.py +0 -0
  54. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/operators.py +0 -0
  55. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/reserved.py +0 -0
  56. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/sql.py +0 -0
  57. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/types.py +0 -0
  58. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/__init__.py +0 -0
  59. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/operators.py +0 -0
  60. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/reserved.py +0 -0
  61. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/sql.py +0 -0
  62. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/types.py +0 -0
  63. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  64. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/operators.py +0 -0
  65. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  66. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/sql.py +0 -0
  67. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/types.py +0 -0
  68. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  69. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  70. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  71. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  72. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/types.py +0 -0
  73. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/tablehelper.py +0 -0
  74. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/__init__.py +0 -0
  75. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/common_db_test.py +0 -0
  76. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/__init__.py +0 -0
  77. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/common.py +0 -0
  78. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_column.py +0 -0
  79. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  80. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_database.py +0 -0
  81. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  82. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  83. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  84. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_result.py +0 -0
  85. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row.py +0 -0
  86. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  87. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  88. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  89. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  90. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  91. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table.py +0 -0
  92. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  93. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  94. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/__init__.py +0 -0
  95. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/common.py +0 -0
  96. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  97. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  98. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  99. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_db_utils.py +0 -0
  100. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres.py +0 -0
  101. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  102. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  103. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_caching.py +0 -0
  104. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  105. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  106. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  107. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  108. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_sql_builder.py +0 -0
  109. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_tablehelper.py +0 -0
  110. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_view_helper.py +0 -0
  111. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/utils.py +0 -0
  112. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/logging.py +0 -0
  113. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/__init__.py +0 -0
  114. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/__init__.py +0 -0
  115. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/iconv.py +0 -0
  116. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/oconv.py +0 -0
  117. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/db.py +0 -0
  118. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/export.py +0 -0
  119. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/format.py +0 -0
  120. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/mail.py +0 -0
  121. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/merge.py +0 -0
  122. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/__init__.py +0 -0
  123. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_db.py +0 -0
  124. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_fix.py +0 -0
  125. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_format.py +0 -0
  126. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_iconv.py +0 -0
  127. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_merge.py +0 -0
  128. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_oconv.py +0 -0
  129. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_original_error.py +0 -0
  130. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_timer.py +0 -0
  131. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/timer.py +0 -0
  132. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tools.py +0 -0
  133. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/__init__.py +0 -0
  134. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/authorizenet_adapter.py +0 -0
  135. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/base_adapter.py +0 -0
  136. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/braintree_adapter.py +0 -0
  137. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/charge_rules.py +0 -0
  138. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/stripe_adapter.py +0 -0
  139. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  140. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  141. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/entry_points.txt +0 -0
  142. {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/top_level.txt +0 -0
  143. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_amplify_build.py +0 -0
  144. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_async_support.py +0 -0
  145. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_batch_operations.py +0 -0
  146. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_concurrency_safety.py +0 -0
  147. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_connection_pool.py +0 -0
  148. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_connection_resilience.py +0 -0
  149. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_decorators.py +0 -0
  150. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_email_processing.py +0 -0
  151. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_iconv_money_to_cents.py +0 -0
  152. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_lambda_handler.py +0 -0
  153. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_lambda_handler_auth.py +0 -0
  154. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_mixins_import.py +0 -0
  155. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_n_plus_one.py +0 -0
  156. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_observability.py +0 -0
  157. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_braintree_adapter.py +0 -0
  158. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_profile_sorting.py +0 -0
  159. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_stripe_adapter.py +0 -0
  160. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_prepared_statements.py +0 -0
  161. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_psycopg3_upgrade.py +0 -0
  162. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_query_cache.py +0 -0
  163. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_batch_update.py +0 -0
  164. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_cache_staleness.py +0 -0
  165. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_dirty_tracking.py +0 -0
  166. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_schema_migrations.py +0 -0
  167. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_security_hardening.py +0 -0
  168. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_spreadsheet_functions.py +0 -0
  169. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_sqs_per_record_transactions.py +0 -0
  170. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  171. {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_table_alter.py +0 -0
  172. {velocity_python-0.1.17 → velocity_python-0.1.20}/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.1.17
3
+ Version: 0.1.20
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
@@ -30,7 +30,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == "excel"
30
30
  Provides-Extra: templates
31
31
  Requires-Dist: jinja2>=3.1.0; extra == "templates"
32
32
  Provides-Extra: pdf
33
- Requires-Dist: weasyprint>=62.0; extra == "pdf"
33
+ Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
34
34
  Provides-Extra: http
35
35
  Requires-Dist: requests>=2.32.0; extra == "http"
36
36
  Provides-Extra: mysql
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.17"
7
+ version = "0.1.20"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -49,7 +49,7 @@ templates = [
49
49
  "jinja2>=3.1.0",
50
50
  ]
51
51
  pdf = [
52
- "weasyprint>=62.0",
52
+ "pdfkit>=1.0.0",
53
53
  ]
54
54
  http = [
55
55
  "requests>=2.32.0",
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.17"
1
+ __version__ = version = "0.1.20"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -239,15 +239,16 @@ class BaseHandler:
239
239
  or "error"
240
240
  )
241
241
 
242
- logger.warning(
243
- "AlertError during action execution",
244
- exc_info=True,
245
- extra={
246
- "handler": self.__class__.__name__,
247
- "action": getattr(local_context, "action", lambda: None)(),
248
- "tx_present": tx is not None,
249
- },
250
- )
242
+ if getattr(exception, "log_to_cloudwatch", False):
243
+ logger.warning(
244
+ "AlertError during action execution",
245
+ exc_info=True,
246
+ extra={
247
+ "handler": self.__class__.__name__,
248
+ "action": getattr(local_context, "action", lambda: None)(),
249
+ "tx_present": tx is not None,
250
+ },
251
+ )
251
252
 
252
253
  # Default behavior: raising AlertError surfaces a UI alert.
253
254
  # If `toast: true` is present in the payload, show a toast instead.
@@ -14,6 +14,7 @@ class AlertError(AppError):
14
14
  title: str | None = None,
15
15
  toast: bool = False,
16
16
  variant: str | None = None,
17
+ log_to_cloudwatch: bool = False,
17
18
  **payload,
18
19
  ):
19
20
  # Backwards compatible:
@@ -24,13 +25,16 @@ class AlertError(AppError):
24
25
  and title is None
25
26
  and toast is False
26
27
  and variant is None
28
+ and log_to_cloudwatch is False
27
29
  and not payload
28
30
  ):
29
31
  super().__init__(message)
32
+ self.log_to_cloudwatch = log_to_cloudwatch
30
33
  return
31
34
 
32
- if isinstance(message, dict) and title is None and toast is False and variant is None and not payload:
35
+ if isinstance(message, dict) and title is None and toast is False and variant is None and log_to_cloudwatch is False and not payload:
33
36
  super().__init__(message)
37
+ self.log_to_cloudwatch = log_to_cloudwatch
34
38
  return
35
39
 
36
40
  data: dict = {}
@@ -49,6 +53,7 @@ class AlertError(AppError):
49
53
  data.update(payload)
50
54
 
51
55
  super().__init__(data)
56
+ self.log_to_cloudwatch = log_to_cloudwatch
52
57
 
53
58
  def get_payload(self):
54
59
  data = self.args[0]
@@ -0,0 +1,217 @@
1
+ """
2
+ HTML-to-PDF generation using pdfkit + wkhtmltopdf.
3
+
4
+ The Python dependency surface is intentionally small: pdfkit wraps an external
5
+ ``wkhtmltopdf`` binary. On Lambda, the binary is typically supplied by the
6
+ support layer at ``/opt/bin/wkhtmltopdf`` with companion shared libraries in
7
+ ``/opt/lib``.
8
+
9
+ Usage::
10
+
11
+ from velocity.misc.pdf import html_to_pdf
12
+
13
+ pdf_bytes = html_to_pdf("<h1>Hello</h1><p>World</p>")
14
+
15
+ pdf_bytes = html_to_pdf(
16
+ html,
17
+ page_size="Letter",
18
+ orientation="landscape",
19
+ margin="0.5in",
20
+ footer_right="Page {page} of {pages}",
21
+ )
22
+
23
+ from velocity.misc.pdf import wrap_html, html_to_pdf
24
+
25
+ html = wrap_html("My Report", "<p>Content here</p>")
26
+ pdf_bytes = html_to_pdf(html)
27
+
28
+ Requires the ``pdf`` extra plus a runnable ``wkhtmltopdf`` binary::
29
+
30
+ pip install velocity-python[pdf]
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ import os
37
+ import shutil
38
+ from typing import Optional
39
+
40
+ logger = logging.getLogger("velocity.misc.pdf")
41
+
42
+ _pdfkit_import_error: Exception | None = None
43
+
44
+ try:
45
+ import pdfkit
46
+ except ImportError as exc:
47
+ _pdfkit_import_error = exc
48
+ pdfkit = None # type: ignore[assignment]
49
+
50
+
51
+ _DEFAULT_BODY_CSS = """\
52
+ body {
53
+ font-family: Arial, Helvetica, sans-serif;
54
+ font-size: 11pt;
55
+ line-height: 1.6;
56
+ color: #222;
57
+ margin: 0;
58
+ padding: 24px;
59
+ }
60
+ h1 { font-size: 20px; margin-bottom: 16px; }
61
+ p { margin: 0 0 12px; line-height: 1.6; }
62
+ ol, ul { padding-left: 24px; }
63
+ li { margin-bottom: 8px; }
64
+ """
65
+
66
+
67
+ def _prepend_env_path(name: str, *paths: str) -> None:
68
+ current = os.environ.get(name, "")
69
+ existing = [path for path in current.split(":") if path]
70
+ merged: list[str] = []
71
+
72
+ for path in paths:
73
+ if path and os.path.isdir(path) and path not in merged:
74
+ merged.append(path)
75
+
76
+ for path in existing:
77
+ if path not in merged:
78
+ merged.append(path)
79
+
80
+ if merged:
81
+ os.environ[name] = ":".join(merged)
82
+
83
+
84
+ def _configure_wkhtml_runtime() -> None:
85
+ if "AWS_LAMBDA_FUNCTION_NAME" not in os.environ:
86
+ return
87
+
88
+ os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")
89
+ os.environ.setdefault("HOME", "/tmp")
90
+ os.environ.setdefault("XDG_RUNTIME_DIR", "/tmp")
91
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
92
+ _prepend_env_path("LD_LIBRARY_PATH", "/opt/lib")
93
+
94
+
95
+ def _find_wkhtmltopdf() -> str | None:
96
+ configured = os.environ.get("WKHTMLTOPDF_PATH")
97
+ candidates = [
98
+ configured,
99
+ shutil.which("wkhtmltopdf"),
100
+ "/opt/bin/wkhtmltopdf",
101
+ "/opt/wkhtmltopdf",
102
+ "/opt/python/bin/wkhtmltopdf",
103
+ "/var/task/bin/wkhtmltopdf",
104
+ ]
105
+
106
+ for candidate in candidates:
107
+ if candidate and os.path.exists(candidate) and os.access(candidate, os.X_OK):
108
+ return candidate
109
+ return None
110
+
111
+
112
+ def _normalize_footer_text(text: str) -> str:
113
+ return text.replace("{page}", "[page]").replace("{pages}", "[topage]")
114
+
115
+
116
+ def _normalize_footer_font_size(value: str) -> str:
117
+ normalized = str(value or "8").strip().lower()
118
+ if normalized.endswith("pt"):
119
+ normalized = normalized[:-2].strip()
120
+ return normalized or "8"
121
+
122
+
123
+ def _require_pdf_backend() -> tuple[object, object]:
124
+ if pdfkit is None:
125
+ base_message = (
126
+ "pdfkit is required for PDF generation. "
127
+ "Install it with: pip install velocity-python[pdf] or pip install pdfkit"
128
+ )
129
+ if _pdfkit_import_error is not None:
130
+ raise ImportError(
131
+ f"{base_message} Root cause: {_pdfkit_import_error!r}"
132
+ ) from _pdfkit_import_error
133
+ raise ImportError(base_message)
134
+
135
+ _configure_wkhtml_runtime()
136
+ wkhtmltopdf_path = _find_wkhtmltopdf()
137
+ if not wkhtmltopdf_path:
138
+ raise ImportError(
139
+ "wkhtmltopdf binary not available for PDF generation. "
140
+ "Expected it in PATH, WKHTMLTOPDF_PATH, or the Lambda layer under /opt/bin."
141
+ )
142
+
143
+ return pdfkit, pdfkit.configuration(wkhtmltopdf=wkhtmltopdf_path)
144
+
145
+
146
+ def html_to_pdf(
147
+ html: str,
148
+ *,
149
+ page_size: str = "Letter",
150
+ orientation: str = "portrait",
151
+ margin: Optional[str] = None,
152
+ margin_top: Optional[str] = None,
153
+ margin_right: Optional[str] = None,
154
+ margin_bottom: Optional[str] = None,
155
+ margin_left: Optional[str] = None,
156
+ footer_right: Optional[str] = None,
157
+ footer_center: Optional[str] = None,
158
+ footer_font_size: str = "8pt",
159
+ ) -> bytes:
160
+ """Convert an HTML string to PDF bytes."""
161
+ pdf_backend, configuration = _require_pdf_backend()
162
+
163
+ options: dict[str, str] = {
164
+ "encoding": "UTF-8",
165
+ "page-size": page_size,
166
+ "orientation": orientation.capitalize(),
167
+ "enable-local-file-access": "",
168
+ "quiet": "",
169
+ }
170
+
171
+ if margin:
172
+ options["margin-top"] = margin
173
+ options["margin-right"] = margin
174
+ options["margin-bottom"] = margin
175
+ options["margin-left"] = margin
176
+ else:
177
+ options["margin-top"] = margin_top or "0.5in"
178
+ options["margin-right"] = margin_right or "0.5in"
179
+ options["margin-bottom"] = margin_bottom or "0.75in"
180
+ options["margin-left"] = margin_left or "0.5in"
181
+
182
+ if footer_right:
183
+ options["footer-right"] = _normalize_footer_text(footer_right)
184
+ if footer_center:
185
+ options["footer-center"] = _normalize_footer_text(footer_center)
186
+ if footer_right or footer_center:
187
+ options["footer-font-size"] = _normalize_footer_font_size(footer_font_size)
188
+
189
+ try:
190
+ return pdf_backend.from_string(
191
+ html,
192
+ False,
193
+ configuration=configuration,
194
+ options=options,
195
+ )
196
+ except Exception as exc:
197
+ logger.exception("wkhtmltopdf PDF generation failed")
198
+ raise RuntimeError(f"PDF generation failed: {exc}") from exc
199
+
200
+
201
+ def wrap_html(title: str, body: str, css: str = "") -> str:
202
+ """Wrap a body fragment in a complete HTML document with default styles."""
203
+ all_css = _DEFAULT_BODY_CSS + "\n" + css
204
+ return f"""<!DOCTYPE html>
205
+ <html>
206
+ <head>
207
+ <meta charset=\"utf-8\">
208
+ <title>{title}</title>
209
+ <style>
210
+ {all_css}
211
+ </style>
212
+ </head>
213
+ <body>
214
+ <h1>{title}</h1>
215
+ {body}
216
+ </body>
217
+ </html>"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.17
3
+ Version: 0.1.20
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
@@ -30,7 +30,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == "excel"
30
30
  Provides-Extra: templates
31
31
  Requires-Dist: jinja2>=3.1.0; extra == "templates"
32
32
  Provides-Extra: pdf
33
- Requires-Dist: weasyprint>=62.0; extra == "pdf"
33
+ Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
34
34
  Provides-Extra: http
35
35
  Requires-Dist: requests>=2.32.0; extra == "http"
36
36
  Provides-Extra: mysql
@@ -34,7 +34,7 @@ stripe>=12.0.0
34
34
  braintree>=4.30.0
35
35
 
36
36
  [pdf]
37
- weasyprint>=62.0
37
+ pdfkit>=1.0.0
38
38
 
39
39
  [postgres]
40
40
  psycopg[binary]>=3.2.0
@@ -0,0 +1,154 @@
1
+ """Tests for velocity.misc.pdf — HTML-to-PDF generation."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from velocity.misc.pdf import _DEFAULT_BODY_CSS, html_to_pdf, wrap_html
8
+
9
+
10
+ class TestWrapHtml:
11
+
12
+ def test_produces_full_document(self):
13
+ result = wrap_html("My Title", "<p>Hello</p>")
14
+ assert "<!DOCTYPE html>" in result
15
+ assert "<title>My Title</title>" in result
16
+ assert "<h1>My Title</h1>" in result
17
+ assert "<p>Hello</p>" in result
18
+
19
+ def test_includes_default_css(self):
20
+ result = wrap_html("T", "<p>body</p>")
21
+ assert "font-family:" in result
22
+ assert _DEFAULT_BODY_CSS.strip()[:30] in result
23
+
24
+ def test_includes_custom_css(self):
25
+ result = wrap_html("T", "<p>body</p>", css=".custom { color: red; }")
26
+ assert ".custom { color: red; }" in result
27
+
28
+ def test_empty_body(self):
29
+ result = wrap_html("T", "")
30
+ assert "<h1>T</h1>" in result
31
+
32
+
33
+ class TestHtmlToPdf:
34
+
35
+ def test_raises_without_pdfkit(self):
36
+ with patch("velocity.misc.pdf.pdfkit", None), patch(
37
+ "velocity.misc.pdf._pdfkit_import_error", ImportError("missing pdfkit")
38
+ ):
39
+ with pytest.raises(ImportError, match="pdfkit is required"):
40
+ html_to_pdf("<h1>test</h1>")
41
+
42
+ def test_raises_without_wkhtmltopdf_binary(self):
43
+ mock_pdfkit = MagicMock()
44
+
45
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
46
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value=None
47
+ ):
48
+ with pytest.raises(ImportError, match="wkhtmltopdf binary not available"):
49
+ html_to_pdf("<h1>test</h1>")
50
+
51
+ def test_calls_pdfkit_with_defaults(self):
52
+ mock_pdfkit = MagicMock()
53
+ mock_config = object()
54
+ mock_pdfkit.configuration.return_value = mock_config
55
+ mock_pdfkit.from_string.return_value = b"%PDF-fake"
56
+
57
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
58
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
59
+ ):
60
+ result = html_to_pdf("<h1>Hello</h1>")
61
+
62
+ assert result == b"%PDF-fake"
63
+ mock_pdfkit.configuration.assert_called_once_with(
64
+ wkhtmltopdf="/opt/bin/wkhtmltopdf"
65
+ )
66
+ mock_pdfkit.from_string.assert_called_once()
67
+ call_args = mock_pdfkit.from_string.call_args
68
+ assert call_args.args[0] == "<h1>Hello</h1>"
69
+ assert call_args.args[1] is False
70
+ assert call_args.kwargs["configuration"] is mock_config
71
+ options = call_args.kwargs["options"]
72
+ assert options["page-size"] == "Letter"
73
+ assert options["orientation"] == "Portrait"
74
+ assert options["margin-top"] == "0.5in"
75
+ assert options["margin-right"] == "0.5in"
76
+ assert options["margin-bottom"] == "0.75in"
77
+ assert options["margin-left"] == "0.5in"
78
+ assert options["enable-local-file-access"] == ""
79
+
80
+ def test_passes_footer_and_margin_options(self):
81
+ mock_pdfkit = MagicMock()
82
+ mock_pdfkit.configuration.return_value = object()
83
+ mock_pdfkit.from_string.return_value = b"%PDF"
84
+
85
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
86
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
87
+ ):
88
+ html_to_pdf(
89
+ "<h1>report</h1>",
90
+ page_size="A4",
91
+ orientation="landscape",
92
+ margin_top="0.5in",
93
+ margin_right="0.45in",
94
+ margin_bottom="0.55in",
95
+ margin_left="0.45in",
96
+ footer_right="Page {page} of {pages}",
97
+ footer_center="CaringCent, LLC",
98
+ footer_font_size="8pt",
99
+ )
100
+
101
+ options = mock_pdfkit.from_string.call_args.kwargs["options"]
102
+ assert options["page-size"] == "A4"
103
+ assert options["orientation"] == "Landscape"
104
+ assert options["margin-top"] == "0.5in"
105
+ assert options["margin-right"] == "0.45in"
106
+ assert options["margin-bottom"] == "0.55in"
107
+ assert options["margin-left"] == "0.45in"
108
+ assert options["footer-right"] == "Page [page] of [topage]"
109
+ assert options["footer-center"] == "CaringCent, LLC"
110
+ assert options["footer-font-size"] == "8"
111
+
112
+ def test_shorthand_margin_applies_to_all_sides(self):
113
+ mock_pdfkit = MagicMock()
114
+ mock_pdfkit.configuration.return_value = object()
115
+ mock_pdfkit.from_string.return_value = b"%PDF"
116
+
117
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
118
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
119
+ ):
120
+ html_to_pdf("<h1>report</h1>", margin="1in", footer_font_size="10pt")
121
+
122
+ options = mock_pdfkit.from_string.call_args.kwargs["options"]
123
+ assert options["margin-top"] == "1in"
124
+ assert options["margin-right"] == "1in"
125
+ assert options["margin-bottom"] == "1in"
126
+ assert options["margin-left"] == "1in"
127
+
128
+ def test_wrap_then_convert(self):
129
+ mock_pdfkit = MagicMock()
130
+ mock_pdfkit.configuration.return_value = object()
131
+ mock_pdfkit.from_string.return_value = b"%PDF-wrapped"
132
+
133
+ html = wrap_html("Report", "<p>Content</p>")
134
+
135
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
136
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
137
+ ):
138
+ result = html_to_pdf(html)
139
+
140
+ assert result == b"%PDF-wrapped"
141
+ passed_html = mock_pdfkit.from_string.call_args.args[0]
142
+ assert "<title>Report</title>" in passed_html
143
+ assert "<p>Content</p>" in passed_html
144
+
145
+ def test_wraps_backend_errors(self):
146
+ mock_pdfkit = MagicMock()
147
+ mock_pdfkit.configuration.return_value = object()
148
+ mock_pdfkit.from_string.side_effect = OSError("boom")
149
+
150
+ with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
151
+ "velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
152
+ ):
153
+ with pytest.raises(RuntimeError, match="PDF generation failed: boom"):
154
+ html_to_pdf("<h1>test</h1>")