velocity-python 0.1.18__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.18/src/velocity_python.egg-info → velocity_python-0.1.20}/PKG-INFO +2 -2
  2. {velocity_python-0.1.18 → velocity_python-0.1.20}/pyproject.toml +2 -2
  3. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.1.20/src/velocity/misc/pdf.py +217 -0
  5. {velocity_python-0.1.18 → velocity_python-0.1.20/src/velocity_python.egg-info}/PKG-INFO +2 -2
  6. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/requires.txt +1 -1
  7. velocity_python-0.1.20/tests/test_pdf.py +154 -0
  8. velocity_python-0.1.18/src/velocity/misc/pdf.py +0 -201
  9. velocity_python-0.1.18/tests/test_pdf.py +0 -188
  10. {velocity_python-0.1.18 → velocity_python-0.1.20}/LICENSE +0 -0
  11. {velocity_python-0.1.18 → velocity_python-0.1.20}/README.md +0 -0
  12. {velocity_python-0.1.18 → velocity_python-0.1.20}/setup.cfg +0 -0
  13. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/__init__.py +0 -0
  14. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/amplify.py +0 -0
  15. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/amplify_build.py +0 -0
  16. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/__init__.py +0 -0
  17. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/base_handler.py +0 -0
  18. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/context.py +0 -0
  19. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/context_factory.py +0 -0
  20. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/exceptions.py +0 -0
  21. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  22. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  23. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  24. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  25. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/perf.py +0 -0
  26. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/response.py +0 -0
  27. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  28. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/__init__.py +0 -0
  29. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  30. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  31. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_response.py +0 -0
  32. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/__init__.py +0 -0
  33. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/__init__.py +0 -0
  34. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/async_support.py +0 -0
  35. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/column.py +0 -0
  36. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/database.py +0 -0
  37. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/decorators.py +0 -0
  38. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/engine.py +0 -0
  39. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/result.py +0 -0
  40. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/row.py +0 -0
  41. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/sequence.py +0 -0
  42. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/table.py +0 -0
  43. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/transaction.py +0 -0
  44. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/view.py +0 -0
  45. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/exceptions.py +0 -0
  46. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/migrations.py +0 -0
  47. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/__init__.py +0 -0
  48. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/__init__.py +0 -0
  49. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/initializer.py +0 -0
  50. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/operators.py +0 -0
  51. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/sql.py +0 -0
  52. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/types.py +0 -0
  53. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/__init__.py +0 -0
  54. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/operators.py +0 -0
  55. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/reserved.py +0 -0
  56. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/sql.py +0 -0
  57. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/types.py +0 -0
  58. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/__init__.py +0 -0
  59. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/operators.py +0 -0
  60. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/reserved.py +0 -0
  61. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/sql.py +0 -0
  62. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/types.py +0 -0
  63. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  64. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/operators.py +0 -0
  65. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  66. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/sql.py +0 -0
  67. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/types.py +0 -0
  68. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  69. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  70. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  71. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  72. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/types.py +0 -0
  73. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/tablehelper.py +0 -0
  74. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/__init__.py +0 -0
  75. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/common_db_test.py +0 -0
  76. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/__init__.py +0 -0
  77. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/common.py +0 -0
  78. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_column.py +0 -0
  79. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  80. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_database.py +0 -0
  81. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  82. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  83. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  84. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_result.py +0 -0
  85. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row.py +0 -0
  86. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  87. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  88. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  89. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  90. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  91. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table.py +0 -0
  92. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  93. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  94. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/__init__.py +0 -0
  95. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/common.py +0 -0
  96. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  97. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  98. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  99. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_db_utils.py +0 -0
  100. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres.py +0 -0
  101. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  102. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  103. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_caching.py +0 -0
  104. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  105. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  106. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  107. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  108. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_sql_builder.py +0 -0
  109. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_tablehelper.py +0 -0
  110. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_view_helper.py +0 -0
  111. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/utils.py +0 -0
  112. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/logging.py +0 -0
  113. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/__init__.py +0 -0
  114. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/__init__.py +0 -0
  115. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/iconv.py +0 -0
  116. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/oconv.py +0 -0
  117. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/db.py +0 -0
  118. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/export.py +0 -0
  119. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/format.py +0 -0
  120. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/mail.py +0 -0
  121. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/merge.py +0 -0
  122. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/__init__.py +0 -0
  123. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_db.py +0 -0
  124. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_fix.py +0 -0
  125. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_format.py +0 -0
  126. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_iconv.py +0 -0
  127. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_merge.py +0 -0
  128. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_oconv.py +0 -0
  129. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_original_error.py +0 -0
  130. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_timer.py +0 -0
  131. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/timer.py +0 -0
  132. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tools.py +0 -0
  133. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/__init__.py +0 -0
  134. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/authorizenet_adapter.py +0 -0
  135. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/base_adapter.py +0 -0
  136. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/braintree_adapter.py +0 -0
  137. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/charge_rules.py +0 -0
  138. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/stripe_adapter.py +0 -0
  139. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  140. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  141. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/entry_points.txt +0 -0
  142. {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/top_level.txt +0 -0
  143. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_amplify_build.py +0 -0
  144. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_async_support.py +0 -0
  145. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_batch_operations.py +0 -0
  146. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_concurrency_safety.py +0 -0
  147. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_connection_pool.py +0 -0
  148. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_connection_resilience.py +0 -0
  149. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_decorators.py +0 -0
  150. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_email_processing.py +0 -0
  151. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_iconv_money_to_cents.py +0 -0
  152. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_lambda_handler.py +0 -0
  153. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_lambda_handler_auth.py +0 -0
  154. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_mixins_import.py +0 -0
  155. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_n_plus_one.py +0 -0
  156. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_observability.py +0 -0
  157. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_braintree_adapter.py +0 -0
  158. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_profile_sorting.py +0 -0
  159. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_stripe_adapter.py +0 -0
  160. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_prepared_statements.py +0 -0
  161. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_psycopg3_upgrade.py +0 -0
  162. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_query_cache.py +0 -0
  163. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_batch_update.py +0 -0
  164. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_cache_staleness.py +0 -0
  165. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_dirty_tracking.py +0 -0
  166. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_schema_migrations.py +0 -0
  167. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_security_hardening.py +0 -0
  168. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_spreadsheet_functions.py +0 -0
  169. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_sqs_per_record_transactions.py +0 -0
  170. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  171. {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_table_alter.py +0 -0
  172. {velocity_python-0.1.18 → 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.18
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.18"
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.18"
1
+ __version__ = version = "0.1.20"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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.18
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>")
@@ -1,201 +0,0 @@
1
- """
2
- HTML-to-PDF generation using WeasyPrint.
3
-
4
- Provides a simple API for converting HTML strings to PDF bytes,
5
- with support for page size, orientation, margins, and page footers.
6
-
7
- Usage::
8
-
9
- from velocity.misc.pdf import html_to_pdf
10
-
11
- # Simple conversion
12
- pdf_bytes = html_to_pdf("<h1>Hello</h1><p>World</p>")
13
-
14
- # With options
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
- # Wrap a body fragment with a full HTML document + default styles
24
- from velocity.misc.pdf import wrap_html, html_to_pdf
25
-
26
- html = wrap_html("My Report", "<p>Content here</p>")
27
- pdf_bytes = html_to_pdf(html)
28
-
29
- Requires the ``pdf`` extra::
30
-
31
- pip install velocity-python[pdf]
32
- """
33
-
34
- from __future__ import annotations
35
-
36
- import logging
37
- import os
38
- from typing import Optional
39
-
40
- logger = logging.getLogger("velocity.misc.pdf")
41
-
42
- # On AWS Lambda, fontconfig needs to know where fonts.conf lives.
43
- # The py-lib-support layer ships fonts/ at /opt/fonts/.
44
- if "AWS_LAMBDA_FUNCTION_NAME" in os.environ:
45
- os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")
46
-
47
- try:
48
- import weasyprint
49
- except (ImportError, OSError):
50
- # OSError: WeasyPrint's ffi.py raises OSError when native libs
51
- # (libgobject, libpango, etc.) cannot be loaded via cffi.dlopen().
52
- weasyprint = None # type: ignore[assignment]
53
-
54
- _DEFAULT_BODY_CSS = """\
55
- body {
56
- font-family: Arial, Helvetica, sans-serif;
57
- font-size: 11pt;
58
- line-height: 1.6;
59
- color: #222;
60
- margin: 0;
61
- padding: 24px;
62
- }
63
- h1 { font-size: 20px; margin-bottom: 16px; }
64
- p { margin: 0 0 12px; line-height: 1.6; }
65
- ol, ul { padding-left: 24px; }
66
- li { margin-bottom: 8px; }
67
- """
68
-
69
-
70
- def _require_weasyprint():
71
- if weasyprint is None:
72
- raise ImportError(
73
- "weasyprint is required for PDF generation. "
74
- "Install it with: pip install velocity-python[pdf]"
75
- )
76
-
77
-
78
- def _build_page_css(
79
- page_size: str = "Letter",
80
- orientation: str = "portrait",
81
- margin: Optional[str] = None,
82
- margin_top: Optional[str] = None,
83
- margin_right: Optional[str] = None,
84
- margin_bottom: Optional[str] = None,
85
- margin_left: Optional[str] = None,
86
- footer_right: Optional[str] = None,
87
- footer_center: Optional[str] = None,
88
- footer_font_size: str = "8pt",
89
- ) -> str:
90
- """Build a ``@page`` CSS rule from the given options."""
91
- parts = [f"size: {page_size} {orientation};"]
92
-
93
- if margin:
94
- parts.append(f"margin: {margin};")
95
- else:
96
- mt = margin_top or "0.5in"
97
- mr = margin_right or "0.5in"
98
- mb = margin_bottom or "0.75in"
99
- ml = margin_left or "0.5in"
100
- parts.append(f"margin: {mt} {mr} {mb} {ml};")
101
-
102
- footer_blocks = []
103
- if footer_right:
104
- content = footer_right.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
105
- content = f'" {content} "'
106
- footer_blocks.append(
107
- f"@bottom-right {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
108
- )
109
- if footer_center:
110
- content = footer_center.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
111
- content = f'" {content} "'
112
- footer_blocks.append(
113
- f"@bottom-center {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
114
- )
115
-
116
- rule_body = "\n ".join(parts)
117
- footer_body = "\n ".join(footer_blocks)
118
- return f"@page {{\n {rule_body}\n {footer_body}\n}}"
119
-
120
-
121
- def html_to_pdf(
122
- html: str,
123
- *,
124
- page_size: str = "Letter",
125
- orientation: str = "portrait",
126
- margin: Optional[str] = None,
127
- margin_top: Optional[str] = None,
128
- margin_right: Optional[str] = None,
129
- margin_bottom: Optional[str] = None,
130
- margin_left: Optional[str] = None,
131
- footer_right: Optional[str] = None,
132
- footer_center: Optional[str] = None,
133
- footer_font_size: str = "8pt",
134
- ) -> bytes:
135
- """
136
- Convert an HTML string to PDF bytes.
137
-
138
- Args:
139
- html: Complete HTML document string.
140
- page_size: CSS page size (e.g. "Letter", "A4"). Default "Letter".
141
- orientation: "portrait" or "landscape". Default "portrait".
142
- margin: Shorthand margin for all sides (e.g. "0.5in").
143
- margin_top/right/bottom/left: Individual margins (override ``margin``).
144
- footer_right: Right-aligned footer text. Use ``{page}`` and ``{pages}``
145
- for page numbering (e.g. ``"Page {page} of {pages}"``).
146
- footer_center: Center-aligned footer text. Same placeholders.
147
- footer_font_size: CSS font size for footer. Default "8pt".
148
-
149
- Returns:
150
- PDF file contents as bytes.
151
-
152
- Raises:
153
- ImportError: If weasyprint is not installed.
154
- """
155
- _require_weasyprint()
156
-
157
- page_css = _build_page_css(
158
- page_size=page_size,
159
- orientation=orientation,
160
- margin=margin,
161
- margin_top=margin_top,
162
- margin_right=margin_right,
163
- margin_bottom=margin_bottom,
164
- margin_left=margin_left,
165
- footer_right=footer_right,
166
- footer_center=footer_center,
167
- footer_font_size=footer_font_size,
168
- )
169
-
170
- css = weasyprint.CSS(string=page_css)
171
- doc = weasyprint.HTML(string=html)
172
- return doc.write_pdf(stylesheets=[css])
173
-
174
-
175
- def wrap_html(title: str, body: str, css: str = "") -> str:
176
- """
177
- Wrap an HTML body fragment in a full HTML document with default styles.
178
-
179
- Args:
180
- title: Document ``<title>`` and ``<h1>`` heading.
181
- body: HTML body content (fragment, not full document).
182
- css: Additional CSS to include after the default body styles.
183
-
184
- Returns:
185
- Complete HTML document string ready for :func:`html_to_pdf`.
186
- """
187
- all_css = _DEFAULT_BODY_CSS + "\n" + css
188
- return f"""<!DOCTYPE html>
189
- <html>
190
- <head>
191
- <meta charset="utf-8">
192
- <title>{title}</title>
193
- <style>
194
- {all_css}
195
- </style>
196
- </head>
197
- <body>
198
- <h1>{title}</h1>
199
- {body}
200
- </body>
201
- </html>"""