velocity-python 0.1.91__tar.gz → 0.1.93__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 (257) hide show
  1. {velocity_python-0.1.91/src/velocity_python.egg-info → velocity_python-0.1.93}/PKG-INFO +3 -1
  2. {velocity_python-0.1.91 → velocity_python-0.1.93}/pyproject.toml +5 -1
  3. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.1.93/src/velocity/misc/pdf.py +494 -0
  5. {velocity_python-0.1.91 → velocity_python-0.1.93/src/velocity_python.egg-info}/PKG-INFO +3 -1
  6. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/requires.txt +3 -0
  7. velocity_python-0.1.93/tests/test_pdf.py +384 -0
  8. velocity_python-0.1.91/src/velocity/misc/pdf.py +0 -217
  9. velocity_python-0.1.91/tests/test_pdf.py +0 -178
  10. {velocity_python-0.1.91 → velocity_python-0.1.93}/LICENSE +0 -0
  11. {velocity_python-0.1.91 → velocity_python-0.1.93}/README.md +0 -0
  12. {velocity_python-0.1.91 → velocity_python-0.1.93}/setup.cfg +0 -0
  13. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/__init__.py +0 -0
  14. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/amplify.py +0 -0
  15. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/amplify_build.py +0 -0
  16. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/__init__.py +0 -0
  17. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/backfill.py +0 -0
  18. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/indexing.py +0 -0
  19. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/references.py +0 -0
  20. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/service.py +0 -0
  21. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/usage_index.py +0 -0
  22. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/__init__.py +0 -0
  23. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/config.py +0 -0
  24. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/operations.py +0 -0
  25. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/policies.py +0 -0
  26. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/reconcile.py +0 -0
  27. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/targets.py +0 -0
  28. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/dirty_pipeline.py +0 -0
  29. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/__init__.py +0 -0
  30. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/base_handler.py +0 -0
  31. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/context.py +0 -0
  32. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/context_factory.py +0 -0
  33. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/exceptions.py +0 -0
  34. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  35. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/masquerade.py +0 -0
  36. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  37. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  38. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  39. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/perf.py +0 -0
  40. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/response.py +0 -0
  41. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  42. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/s3.py +0 -0
  43. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/ssm_config.py +0 -0
  44. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/__init__.py +0 -0
  45. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  46. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  47. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  48. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  49. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  50. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  51. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_response.py +0 -0
  52. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/__init__.py +0 -0
  53. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/__init__.py +0 -0
  54. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/async_support.py +0 -0
  55. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/column.py +0 -0
  56. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/database.py +0 -0
  57. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/decorators.py +0 -0
  58. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/engine.py +0 -0
  59. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/jsonproxy.py +0 -0
  60. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/result.py +0 -0
  61. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/row.py +0 -0
  62. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/sequence.py +0 -0
  63. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/table.py +0 -0
  64. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/transaction.py +0 -0
  65. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/view.py +0 -0
  66. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/exceptions.py +0 -0
  67. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/maintenance.py +0 -0
  68. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/migrations.py +0 -0
  69. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/__init__.py +0 -0
  70. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/__init__.py +0 -0
  71. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/initializer.py +0 -0
  72. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/operators.py +0 -0
  73. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/sql.py +0 -0
  74. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/types.py +0 -0
  75. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/__init__.py +0 -0
  76. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/operators.py +0 -0
  77. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/reserved.py +0 -0
  78. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/sql.py +0 -0
  79. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/types.py +0 -0
  80. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/__init__.py +0 -0
  81. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/operators.py +0 -0
  82. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/reserved.py +0 -0
  83. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/sql.py +0 -0
  84. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/types.py +0 -0
  85. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  86. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/operators.py +0 -0
  87. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  88. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/sql.py +0 -0
  89. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/types.py +0 -0
  90. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  91. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  92. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  93. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  94. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/types.py +0 -0
  95. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/tablehelper.py +0 -0
  96. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/__init__.py +0 -0
  97. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/common_db_test.py +0 -0
  98. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/__init__.py +0 -0
  99. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/common.py +0 -0
  100. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/conftest.py +0 -0
  101. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_column.py +0 -0
  102. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  103. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_database.py +0 -0
  104. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  105. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  106. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  107. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_result.py +0 -0
  108. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_row.py +0 -0
  109. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  110. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  111. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  112. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  113. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  114. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_table.py +0 -0
  115. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  116. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  117. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/__init__.py +0 -0
  118. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/common.py +0 -0
  119. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  120. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  121. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  122. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_db_utils.py +0 -0
  123. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_postgres.py +0 -0
  124. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  125. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  126. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_result_caching.py +0 -0
  127. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  128. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  129. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  130. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  131. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_sql_builder.py +0 -0
  132. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_tablehelper.py +0 -0
  133. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_view_helper.py +0 -0
  134. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/utils.py +0 -0
  135. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/logging.py +0 -0
  136. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/__init__.py +0 -0
  137. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/__init__.py +0 -0
  138. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/iconv.py +0 -0
  139. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/oconv.py +0 -0
  140. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/db.py +0 -0
  141. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/export.py +0 -0
  142. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/format.py +0 -0
  143. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/mail.py +0 -0
  144. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/merge.py +0 -0
  145. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/__init__.py +0 -0
  146. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_db.py +0 -0
  147. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_fix.py +0 -0
  148. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_format.py +0 -0
  149. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_iconv.py +0 -0
  150. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_merge.py +0 -0
  151. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_oconv.py +0 -0
  152. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_original_error.py +0 -0
  153. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_timer.py +0 -0
  154. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/timer.py +0 -0
  155. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tools.py +0 -0
  156. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/__init__.py +0 -0
  157. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/__init__.py +0 -0
  158. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/capabilities.py +0 -0
  159. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/store.py +0 -0
  160. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/where.py +0 -0
  161. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/exceptions.py +0 -0
  162. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/__init__.py +0 -0
  163. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/base/__init__.py +0 -0
  164. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  165. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  166. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  167. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  168. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  169. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  170. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  171. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  172. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  173. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  174. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  175. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  176. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/__init__.py +0 -0
  177. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/authorizenet_adapter.py +0 -0
  178. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/authorizenet_mirror.py +0 -0
  179. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/base_adapter.py +0 -0
  180. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/braintree_adapter.py +0 -0
  181. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/braintree_mirror.py +0 -0
  182. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/charge_rules.py +0 -0
  183. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/stripe_adapter.py +0 -0
  184. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/stripe_mirror.py +0 -0
  185. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  186. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  187. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/entry_points.txt +0 -0
  188. {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/top_level.txt +0 -0
  189. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_activity_tracking_aborted_tx.py +0 -0
  190. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_amplify_build.py +0 -0
  191. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_asset_indexing.py +0 -0
  192. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_asset_references.py +0 -0
  193. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_assets_service.py +0 -0
  194. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_async_support.py +0 -0
  195. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_batch_operations.py +0 -0
  196. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_column_tx_arg.py +0 -0
  197. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_concurrency_safety.py +0 -0
  198. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_connection_pool.py +0 -0
  199. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_connection_resilience.py +0 -0
  200. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_context_job_descriptions.py +0 -0
  201. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_db_credentials_ssm_cascade.py +0 -0
  202. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_db_maintenance.py +0 -0
  203. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_decorators.py +0 -0
  204. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_delete_hook_fk_blocked.py +0 -0
  205. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_dirty_pipeline_fast_path.py +0 -0
  206. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_email_processing.py +0 -0
  207. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_enqueue_send_failures.py +0 -0
  208. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_get_cognito_user_provider.py +0 -0
  209. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_http_handler_rollback.py +0 -0
  210. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_iconv_money_to_cents.py +0 -0
  211. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_identifier_injection_guard.py +0 -0
  212. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_json_columns.py +0 -0
  213. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_jsonb_dict_adapter.py +0 -0
  214. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler.py +0 -0
  215. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler_auth.py +0 -0
  216. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler_masquerade.py +0 -0
  217. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_masquerade_grant.py +0 -0
  218. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_mixins_import.py +0 -0
  219. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_n_plus_one.py +0 -0
  220. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_dynamodb.py +0 -0
  221. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_memory.py +0 -0
  222. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_mongodb.py +0 -0
  223. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_redis.py +0 -0
  224. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_translation.py +0 -0
  225. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_where.py +0 -0
  226. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_observability.py +0 -0
  227. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_on_commit.py +0 -0
  228. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_authorizenet_adapter.py +0 -0
  229. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_braintree_adapter.py +0 -0
  230. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_braintree_mirror.py +0 -0
  231. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_mirror_empty_table.py +0 -0
  232. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_profile_sorting.py +0 -0
  233. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_stripe_adapter.py +0 -0
  234. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_prepared_statements.py +0 -0
  235. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_psycopg3_upgrade.py +0 -0
  236. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_query_cache.py +0 -0
  237. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_restricted_direct_tables.py +0 -0
  238. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_retry_side_effect_guard.py +0 -0
  239. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_return_default_safety.py +0 -0
  240. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_row_batch_update.py +0 -0
  241. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_row_cache_staleness.py +0 -0
  242. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_schema_migrations.py +0 -0
  243. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_security_hardening.py +0 -0
  244. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_server_cursor.py +0 -0
  245. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_single_autocommit_safety.py +0 -0
  246. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_spreadsheet_functions.py +0 -0
  247. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sqlite_backend.py +0 -0
  248. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sqs_per_record_transactions.py +0 -0
  249. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_ssm_config.py +0 -0
  250. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_store_user_data.py +0 -0
  251. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  252. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_table_alter.py +0 -0
  253. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_class_wrapping.py +0 -0
  254. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_commit_and_ownership.py +0 -0
  255. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_edge_cases.py +0 -0
  256. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_where_clause_validation.py +0 -0
  257. {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_write_hook_create_flow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.91
3
+ Version: 0.1.93
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
@@ -35,6 +35,8 @@ Provides-Extra: templates
35
35
  Requires-Dist: jinja2>=3.1.0; extra == "templates"
36
36
  Provides-Extra: pdf
37
37
  Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
38
+ Provides-Extra: pdf-weasyprint
39
+ Requires-Dist: weasyprint>=60.0; extra == "pdf-weasyprint"
38
40
  Provides-Extra: http
39
41
  Requires-Dist: requests>=2.32.0; extra == "http"
40
42
  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.91"
7
+ version = "0.1.93"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -57,6 +57,10 @@ templates = [
57
57
  pdf = [
58
58
  "pdfkit>=1.0.0",
59
59
  ]
60
+ # Maintained HTML→PDF engine; native libs (Pango/Cairo) come from the OS or a layer.
61
+ pdf-weasyprint = [
62
+ "weasyprint>=60.0",
63
+ ]
60
64
  http = [
61
65
  "requests>=2.32.0",
62
66
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.91"
1
+ __version__ = version = "0.1.93"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -0,0 +1,494 @@
1
+ """
2
+ HTML-to-PDF rendering behind a swappable adapter.
3
+
4
+ Public surface
5
+ --------------
6
+ ``html_to_pdf(html, **page_options)`` — the facade every consumer should call.
7
+ It picks a renderer (see *Engine selection*) and returns PDF bytes. Its keyword
8
+ contract is unchanged from the original pdfkit-only implementation, so existing
9
+ callers keep working; ``engine=``, ``base_url=`` and ``extra_css=`` are additive.
10
+
11
+ ``BasePdfRenderer`` — the adapter interface. A renderer turns
12
+ ``(html, PageOptions, base_url, extra_css)`` into bytes. Two ship here:
13
+
14
+ * ``WkhtmltopdfRenderer`` — the original path: pdfkit + the external
15
+ ``wkhtmltopdf`` binary (``/opt/bin/wkhtmltopdf`` + ``/opt/lib`` on Lambda).
16
+ That binary is an Amazon Linux 2 build and cannot load on Amazon Linux 2023
17
+ runtimes (python3.12+; ``libssl.so.10`` is gone), and upstream was archived in
18
+ 2023 — it is kept, unchanged in behaviour, for existing python3.11/AL2 stacks.
19
+ * ``WeasyPrintRenderer`` — WeasyPrint, a maintained pure-Python engine
20
+ (Pango/Cairo/HarfBuzz underneath). On Lambda the native libraries come from a
21
+ layer (e.g. kotify/cloud-print-utils' AL2023 build) with
22
+ ``LD_LIBRARY_PATH=/opt/lib`` and ``FONTCONFIG_PATH=/opt/fonts``. Page size,
23
+ orientation, margins and footers become CSS Paged Media (``@page``).
24
+ WeasyPrint honours CSS pixels literally — it has no "smart shrinking" — so
25
+ documents authored wider than the page for wkhtmltopdf must be restyled.
26
+
27
+ Swapping the backend later (Chromium, a PDF microservice, a SaaS API, …) means
28
+ subclassing ``BasePdfRenderer`` and calling ``register_renderer(name, instance)``;
29
+ no caller changes.
30
+
31
+ Engine selection (``get_renderer``)
32
+ -----------------------------------
33
+ 1. an explicit ``engine="..."`` argument,
34
+ 2. else the ``VELOCITY_PDF_ENGINE`` environment variable,
35
+ 3. else the first *available* renderer in preference order (WeasyPrint, then
36
+ wkhtmltopdf) — which preserves the historical behaviour on stacks that only
37
+ have pdfkit + the binary.
38
+
39
+ Usage::
40
+
41
+ from velocity.misc.pdf import html_to_pdf
42
+
43
+ pdf_bytes = html_to_pdf(html, page_size="Letter", margin="0in",
44
+ base_url="/opt/templates") # auto engine
45
+ pdf_bytes = html_to_pdf(html, engine="weasyprint",
46
+ footer_right="Page {page} of {pages}")
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import logging
52
+ import os
53
+ import re
54
+ import shutil
55
+ from abc import ABC, abstractmethod
56
+ from dataclasses import dataclass, field
57
+ from typing import Dict, List, Optional
58
+
59
+ logger = logging.getLogger("velocity.misc.pdf")
60
+
61
+ _pdfkit_import_error: Exception | None = None
62
+
63
+ try:
64
+ import pdfkit
65
+ except ImportError as exc:
66
+ _pdfkit_import_error = exc
67
+ pdfkit = None # type: ignore[assignment]
68
+
69
+
70
+ _DEFAULT_BODY_CSS = """\
71
+ body {
72
+ font-family: Arial, Helvetica, sans-serif;
73
+ font-size: 11pt;
74
+ line-height: 1.6;
75
+ color: #222;
76
+ margin: 0;
77
+ padding: 24px;
78
+ }
79
+ h1 { font-size: 20px; margin-bottom: 16px; }
80
+ p { margin: 0 0 12px; line-height: 1.6; }
81
+ ol, ul { padding-left: 24px; }
82
+ li { margin-bottom: 8px; }
83
+ """
84
+
85
+
86
+ # ── Page options (engine-neutral) ──────────────────────────────────────────────
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class PageOptions:
91
+ """Everything a renderer needs to know about the page, engine-neutral.
92
+
93
+ Margins default to the historical pdfkit values (0.5in / 0.5in / 0.75in /
94
+ 0.5in) unless ``margin`` (all four) or individual sides are given. Footer
95
+ text may contain ``{page}`` / ``{pages}`` placeholders.
96
+ """
97
+
98
+ page_size: str = "Letter"
99
+ orientation: str = "portrait"
100
+ margin: Optional[str] = None
101
+ margin_top: Optional[str] = None
102
+ margin_right: Optional[str] = None
103
+ margin_bottom: Optional[str] = None
104
+ margin_left: Optional[str] = None
105
+ footer_right: Optional[str] = None
106
+ footer_center: Optional[str] = None
107
+ footer_font_size: str = "8pt"
108
+
109
+ def margins(self) -> tuple[str, str, str, str]:
110
+ if self.margin:
111
+ return (self.margin,) * 4
112
+ return (
113
+ self.margin_top or "0.5in",
114
+ self.margin_right or "0.5in",
115
+ self.margin_bottom or "0.75in",
116
+ self.margin_left or "0.5in",
117
+ )
118
+
119
+ @property
120
+ def landscape(self) -> bool:
121
+ return self.orientation.strip().lower() == "landscape"
122
+
123
+
124
+ # ── Adapter interface ──────────────────────────────────────────────────────────
125
+
126
+
127
+ class BasePdfRenderer(ABC):
128
+ """One HTML→PDF backend. Subclass, implement ``render``/``available``, then
129
+ ``register_renderer(name, instance)``."""
130
+
131
+ #: Registry key and the value accepted by ``engine=`` / VELOCITY_PDF_ENGINE.
132
+ name: str = "base"
133
+
134
+ @abstractmethod
135
+ def available(self) -> bool:
136
+ """True when this backend can run in the current process."""
137
+
138
+ @abstractmethod
139
+ def render(
140
+ self,
141
+ html: str,
142
+ page: PageOptions,
143
+ *,
144
+ base_url: Optional[str] = None,
145
+ extra_css: Optional[str] = None,
146
+ ) -> bytes:
147
+ """Return PDF bytes. Raise ``ImportError`` when the backend is missing
148
+ and ``RuntimeError`` when rendering fails."""
149
+
150
+
151
+ # ── wkhtmltopdf adapter (original behaviour, unchanged) ────────────────────────
152
+
153
+
154
+ def _prepend_env_path(name: str, *paths: str) -> None:
155
+ existing = os.environ.get(name, "")
156
+ parts = [p for p in paths if p and os.path.isdir(p)]
157
+ if not parts:
158
+ return
159
+ current = [p for p in existing.split(os.pathsep) if p]
160
+ merged = parts + [p for p in current if p not in parts]
161
+ os.environ[name] = os.pathsep.join(merged)
162
+
163
+
164
+ def _configure_wkhtml_runtime() -> None:
165
+ # Lambda layers put the binary in /opt/bin and its shared libs in /opt/lib.
166
+ _prepend_env_path("PATH", "/opt/bin")
167
+ _prepend_env_path("LD_LIBRARY_PATH", "/opt/lib")
168
+ if os.path.isdir("/opt/fonts") and not os.environ.get("FONTCONFIG_PATH"):
169
+ os.environ["FONTCONFIG_PATH"] = "/opt/fonts"
170
+
171
+
172
+ def _find_wkhtmltopdf() -> str | None:
173
+ explicit = os.environ.get("WKHTMLTOPDF_PATH") or os.environ.get("wkhtmltopdf")
174
+ if explicit and os.path.isfile(explicit) and os.access(explicit, os.X_OK):
175
+ return explicit
176
+ found = shutil.which("wkhtmltopdf")
177
+ if found:
178
+ return found
179
+ layer_binary = "/opt/bin/wkhtmltopdf"
180
+ if os.path.isfile(layer_binary) and os.access(layer_binary, os.X_OK):
181
+ return layer_binary
182
+ return None
183
+
184
+
185
+ def _normalize_footer_text(text: str) -> str:
186
+ return text.replace("{page}", "[page]").replace("{pages}", "[topage]")
187
+
188
+
189
+ def _normalize_footer_font_size(value: str) -> str:
190
+ text = str(value).strip()
191
+ if text.endswith("pt"):
192
+ text = text[:-2]
193
+ return text or "8"
194
+
195
+
196
+ def _require_pdf_backend() -> tuple[object, object]:
197
+ if pdfkit is None:
198
+ base_message = (
199
+ "pdfkit is required for PDF generation. "
200
+ "Install it with: pip install velocity-python[pdf] or pip install pdfkit"
201
+ )
202
+ if _pdfkit_import_error is not None:
203
+ raise ImportError(
204
+ f"{base_message} Root cause: {_pdfkit_import_error!r}"
205
+ ) from _pdfkit_import_error
206
+ raise ImportError(base_message)
207
+
208
+ _configure_wkhtml_runtime()
209
+ wkhtmltopdf_path = _find_wkhtmltopdf()
210
+ if not wkhtmltopdf_path:
211
+ raise ImportError(
212
+ "wkhtmltopdf binary not available for PDF generation. "
213
+ "Expected it in PATH, WKHTMLTOPDF_PATH, or the Lambda layer under /opt/bin."
214
+ )
215
+
216
+ return pdfkit, pdfkit.configuration(wkhtmltopdf=wkhtmltopdf_path)
217
+
218
+
219
+ class WkhtmltopdfRenderer(BasePdfRenderer):
220
+ """pdfkit + external wkhtmltopdf binary. Behaviour is byte-for-byte the
221
+ original ``html_to_pdf``; ``base_url`` is not supported by this backend and
222
+ is ignored (wkhtmltopdf resolves relative assets itself with
223
+ ``enable-local-file-access``)."""
224
+
225
+ name = "wkhtmltopdf"
226
+
227
+ def available(self) -> bool:
228
+ if pdfkit is None:
229
+ return False
230
+ _configure_wkhtml_runtime()
231
+ return _find_wkhtmltopdf() is not None
232
+
233
+ def options(self, page: PageOptions) -> dict[str, str]:
234
+ top, right, bottom, left = page.margins()
235
+ options: dict[str, str] = {
236
+ "encoding": "UTF-8",
237
+ "page-size": page.page_size,
238
+ "orientation": page.orientation.capitalize(),
239
+ "enable-local-file-access": "",
240
+ "quiet": "",
241
+ "margin-top": top,
242
+ "margin-right": right,
243
+ "margin-bottom": bottom,
244
+ "margin-left": left,
245
+ }
246
+ if page.footer_right:
247
+ options["footer-right"] = _normalize_footer_text(page.footer_right)
248
+ if page.footer_center:
249
+ options["footer-center"] = _normalize_footer_text(page.footer_center)
250
+ if page.footer_right or page.footer_center:
251
+ options["footer-font-size"] = _normalize_footer_font_size(page.footer_font_size)
252
+ return options
253
+
254
+ def render(self, html, page, *, base_url=None, extra_css=None):
255
+ pdf_backend, configuration = _require_pdf_backend()
256
+ if extra_css:
257
+ html = _inject_css(html, extra_css)
258
+ try:
259
+ return pdf_backend.from_string(
260
+ html, False, configuration=configuration, options=self.options(page)
261
+ )
262
+ except Exception as exc:
263
+ logger.exception("wkhtmltopdf PDF generation failed")
264
+ raise RuntimeError(f"PDF generation failed: {exc}") from exc
265
+
266
+
267
+ # ── WeasyPrint adapter ─────────────────────────────────────────────────────────
268
+
269
+ _weasyprint_import_error: Exception | None = None
270
+ _FOOTER_TOKEN_RE = re.compile(r"(\{page\}|\{pages\})")
271
+
272
+
273
+ def _load_weasyprint():
274
+ """Import WeasyPrint lazily: it loads Pango/Cairo at import time, and a
275
+ missing shared library surfaces as OSError rather than ImportError."""
276
+ global _weasyprint_import_error
277
+ try:
278
+ import weasyprint # type: ignore[import-not-found]
279
+
280
+ return weasyprint
281
+ except Exception as exc:
282
+ _weasyprint_import_error = exc
283
+ return None
284
+
285
+
286
+ def _css_font_size(value: str) -> str:
287
+ text = str(value).strip() or "8pt"
288
+ return text if re.search(r"[a-z%]$", text) else f"{text}pt"
289
+
290
+
291
+ def _footer_content(text: str) -> str:
292
+ """'Page {page} of {pages}' → CSS ``content`` with page counters."""
293
+ pieces: list[str] = []
294
+ for token in _FOOTER_TOKEN_RE.split(text):
295
+ if not token:
296
+ continue
297
+ if token == "{page}":
298
+ pieces.append("counter(page)")
299
+ elif token == "{pages}":
300
+ pieces.append("counter(pages)")
301
+ else:
302
+ pieces.append('"' + token.replace("\\", "\\\\").replace('"', '\\"') + '"')
303
+ return " ".join(pieces) if pieces else '""'
304
+
305
+
306
+ #: wkhtmltopdf/Qt page names that CSS Paged Media has no keyword for, as
307
+ #: portrait (width height). CSS-native names (Letter, Legal, A4, …) pass through.
308
+ _CSS_PAGE_SIZES = {
309
+ "comm10e": ("4.125in", "9.5in"),
310
+ "comm10": ("4.125in", "9.5in"),
311
+ "tabloid": ("11in", "17in"),
312
+ "executive": ("7.25in", "10.5in"),
313
+ }
314
+
315
+
316
+ def _css_page_size(page: PageOptions) -> str:
317
+ named = _CSS_PAGE_SIZES.get(page.page_size.strip().lower())
318
+ if named:
319
+ width, height = named
320
+ # CSS allows `landscape` only with named sizes; swap explicit lengths.
321
+ return f"{height} {width}" if page.landscape else f"{width} {height}"
322
+ return page.page_size.strip() + (" landscape" if page.landscape else "")
323
+
324
+
325
+ def page_css(page: PageOptions) -> str:
326
+ """Translate PageOptions into a CSS ``@page`` rule (pure; unit-tested)."""
327
+ size = _css_page_size(page)
328
+ top, right, bottom, left = page.margins()
329
+ parts = [f"size: {size};", f"margin: {top} {right} {bottom} {left};"]
330
+ for slot, text in (("bottom-right", page.footer_right), ("bottom-center", page.footer_center)):
331
+ if text:
332
+ parts.append(
333
+ f"@{slot} {{ content: {_footer_content(text)}; "
334
+ f"font-size: {_css_font_size(page.footer_font_size)}; }}"
335
+ )
336
+ return "@page { " + " ".join(parts) + " }"
337
+
338
+
339
+ class WeasyPrintRenderer(BasePdfRenderer):
340
+ name = "weasyprint"
341
+
342
+ def available(self) -> bool:
343
+ return _load_weasyprint() is not None
344
+
345
+ def render(self, html, page, *, base_url=None, extra_css=None):
346
+ weasyprint = _load_weasyprint()
347
+ if weasyprint is None:
348
+ raise ImportError(
349
+ "WeasyPrint is required for engine='weasyprint'. Install it with: "
350
+ "pip install velocity-python[pdf-weasyprint] (on Lambda, attach a layer "
351
+ "that carries its Pango/Cairo libraries and set LD_LIBRARY_PATH=/opt/lib, "
352
+ f"FONTCONFIG_PATH=/opt/fonts). Root cause: {_weasyprint_import_error!r}"
353
+ )
354
+ css_text = page_css(page)
355
+ if extra_css:
356
+ css_text += "\n" + extra_css
357
+ try:
358
+ document = weasyprint.HTML(string=html, base_url=base_url)
359
+ return document.write_pdf(stylesheets=[weasyprint.CSS(string=css_text)])
360
+ except Exception as exc:
361
+ logger.exception("WeasyPrint PDF generation failed")
362
+ raise RuntimeError(f"PDF generation failed: {exc}") from exc
363
+
364
+
365
+ # ── Registry + facade ──────────────────────────────────────────────────────────
366
+
367
+ #: Preference order for auto-selection: maintained engine first, legacy second.
368
+ _RENDERERS: Dict[str, BasePdfRenderer] = {
369
+ WeasyPrintRenderer.name: WeasyPrintRenderer(),
370
+ WkhtmltopdfRenderer.name: WkhtmltopdfRenderer(),
371
+ }
372
+
373
+
374
+ def register_renderer(name: str, renderer: BasePdfRenderer, *, preferred: bool = False) -> None:
375
+ """Add (or replace) a backend. ``preferred=True`` makes it the first choice
376
+ for auto-selection."""
377
+ key = name.strip().lower()
378
+ if preferred:
379
+ rest = {k: v for k, v in _RENDERERS.items() if k != key}
380
+ _RENDERERS.clear()
381
+ _RENDERERS[key] = renderer
382
+ _RENDERERS.update(rest)
383
+ else:
384
+ _RENDERERS[key] = renderer
385
+
386
+
387
+ def registered_renderers() -> List[str]:
388
+ return list(_RENDERERS)
389
+
390
+
391
+ def available_pdf_engines() -> List[str]:
392
+ """Engines usable in this process, in preference order."""
393
+ return [name for name, r in _RENDERERS.items() if r.available()]
394
+
395
+
396
+ def get_renderer(engine: Optional[str] = None) -> BasePdfRenderer:
397
+ """Resolve the backend: explicit arg → VELOCITY_PDF_ENGINE → first available."""
398
+ requested = (engine or os.environ.get("VELOCITY_PDF_ENGINE") or "").strip().lower()
399
+ if requested:
400
+ try:
401
+ return _RENDERERS[requested]
402
+ except KeyError:
403
+ raise ValueError(
404
+ f"Unknown PDF engine {requested!r}; registered: {', '.join(_RENDERERS)}"
405
+ ) from None
406
+ for renderer in _RENDERERS.values():
407
+ if renderer.available():
408
+ return renderer
409
+ # Nothing available: fall through to the legacy backend so its ImportError
410
+ # message (the one existing callers/tests know) is what surfaces.
411
+ return _RENDERERS[WkhtmltopdfRenderer.name]
412
+
413
+
414
+ def html_to_pdf(
415
+ html: str,
416
+ *,
417
+ page_size: str = "Letter",
418
+ orientation: str = "portrait",
419
+ margin: Optional[str] = None,
420
+ margin_top: Optional[str] = None,
421
+ margin_right: Optional[str] = None,
422
+ margin_bottom: Optional[str] = None,
423
+ margin_left: Optional[str] = None,
424
+ footer_right: Optional[str] = None,
425
+ footer_center: Optional[str] = None,
426
+ footer_font_size: str = "8pt",
427
+ engine: Optional[str] = None,
428
+ base_url: Optional[str] = None,
429
+ extra_css: Optional[str] = None,
430
+ ) -> bytes:
431
+ """Convert an HTML string to PDF bytes via the selected renderer.
432
+
433
+ Keyword contract is unchanged from the pdfkit-only original; ``engine``,
434
+ ``base_url`` and ``extra_css`` are additive. See the module docstring for
435
+ engine selection.
436
+ """
437
+ page = PageOptions(
438
+ page_size=page_size,
439
+ orientation=orientation,
440
+ margin=margin,
441
+ margin_top=margin_top,
442
+ margin_right=margin_right,
443
+ margin_bottom=margin_bottom,
444
+ margin_left=margin_left,
445
+ footer_right=footer_right,
446
+ footer_center=footer_center,
447
+ footer_font_size=footer_font_size,
448
+ )
449
+ return get_renderer(engine).render(html, page, base_url=base_url, extra_css=extra_css)
450
+
451
+
452
+ def _inject_css(html: str, css: str) -> str:
453
+ """Best-effort: put extra CSS into <head> (or prepend) for engines that take
454
+ styles only from the document."""
455
+ block = f"<style>{css}</style>"
456
+ lowered = html.lower()
457
+ idx = lowered.find("</head>")
458
+ if idx != -1:
459
+ return html[:idx] + block + html[idx:]
460
+ return block + html
461
+
462
+
463
+ def wrap_html(title: str, body: str, css: str = "") -> str:
464
+ """Wrap a body fragment in a complete HTML document with default styles."""
465
+ all_css = _DEFAULT_BODY_CSS + "\n" + css
466
+ return f"""<!DOCTYPE html>
467
+ <html>
468
+ <head>
469
+ <meta charset=\"utf-8\">
470
+ <title>{title}</title>
471
+ <style>
472
+ {all_css}
473
+ </style>
474
+ </head>
475
+ <body>
476
+ <h1>{title}</h1>
477
+ {body}
478
+ </body>
479
+ </html>"""
480
+
481
+
482
+ __all__ = [
483
+ "BasePdfRenderer",
484
+ "PageOptions",
485
+ "WeasyPrintRenderer",
486
+ "WkhtmltopdfRenderer",
487
+ "available_pdf_engines",
488
+ "get_renderer",
489
+ "html_to_pdf",
490
+ "page_css",
491
+ "register_renderer",
492
+ "registered_renderers",
493
+ "wrap_html",
494
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.91
3
+ Version: 0.1.93
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
@@ -35,6 +35,8 @@ Provides-Extra: templates
35
35
  Requires-Dist: jinja2>=3.1.0; extra == "templates"
36
36
  Provides-Extra: pdf
37
37
  Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
38
+ Provides-Extra: pdf-weasyprint
39
+ Requires-Dist: weasyprint>=60.0; extra == "pdf-weasyprint"
38
40
  Provides-Extra: http
39
41
  Requires-Dist: requests>=2.32.0; extra == "http"
40
42
  Provides-Extra: mysql
@@ -55,6 +55,9 @@ xmltodict>=0.13.0
55
55
  [pdf]
56
56
  pdfkit>=1.0.0
57
57
 
58
+ [pdf-weasyprint]
59
+ weasyprint>=60.0
60
+
58
61
  [postgres]
59
62
  psycopg[binary]>=3.2.0
60
63