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.
- {velocity_python-0.1.91/src/velocity_python.egg-info → velocity_python-0.1.93}/PKG-INFO +3 -1
- {velocity_python-0.1.91 → velocity_python-0.1.93}/pyproject.toml +5 -1
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.93/src/velocity/misc/pdf.py +494 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93/src/velocity_python.egg-info}/PKG-INFO +3 -1
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/requires.txt +3 -0
- velocity_python-0.1.93/tests/test_pdf.py +384 -0
- velocity_python-0.1.91/src/velocity/misc/pdf.py +0 -217
- velocity_python-0.1.91/tests/test_pdf.py +0 -178
- {velocity_python-0.1.91 → velocity_python-0.1.93}/LICENSE +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/README.md +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/setup.cfg +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_observability.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.91 → velocity_python-0.1.93}/tests/test_where_clause_validation.py +0 -0
- {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.
|
|
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.
|
|
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
|
]
|
|
@@ -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.
|
|
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
|