velocity-python 0.1.11__tar.gz → 0.1.13__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.11/src/velocity_python.egg-info → velocity_python-0.1.13}/PKG-INFO +4 -2
- {velocity_python-0.1.11 → velocity_python-0.1.13}/pyproject.toml +5 -2
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/migrations.py +24 -43
- velocity_python-0.1.13/src/velocity/misc/pdf.py +199 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13/src/velocity_python.egg-info}/PKG-INFO +4 -2
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity_python.egg-info/SOURCES.txt +2 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity_python.egg-info/requires.txt +4 -1
- velocity_python-0.1.13/tests/test_pdf.py +188 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_schema_migrations.py +37 -38
- {velocity_python-0.1.11 → velocity_python-0.1.13}/LICENSE +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/README.md +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/setup.cfg +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/formbuilder/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/formbuilder/reshaper.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/validators/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/app/validators/formbuilder_template.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/demo_profiles.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/profiles.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/router.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_formbuilder_reshaper.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_formbuilder_template_validator.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_observability.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_payment_demo_profiles.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_payment_profiles.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_payment_router.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.11 → velocity_python-0.1.13}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
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
|
|
@@ -29,6 +29,8 @@ Provides-Extra: excel
|
|
|
29
29
|
Requires-Dist: openpyxl>=3.1.0; extra == "excel"
|
|
30
30
|
Provides-Extra: templates
|
|
31
31
|
Requires-Dist: jinja2>=3.1.0; extra == "templates"
|
|
32
|
+
Provides-Extra: pdf
|
|
33
|
+
Requires-Dist: weasyprint>=62.0; extra == "pdf"
|
|
32
34
|
Provides-Extra: http
|
|
33
35
|
Requires-Dist: requests>=2.32.0; extra == "http"
|
|
34
36
|
Provides-Extra: mysql
|
|
@@ -41,7 +43,7 @@ Provides-Extra: payment
|
|
|
41
43
|
Requires-Dist: stripe>=12.0.0; extra == "payment"
|
|
42
44
|
Requires-Dist: braintree>=4.30.0; extra == "payment"
|
|
43
45
|
Provides-Extra: all
|
|
44
|
-
Requires-Dist: velocity-python[aws,excel,http,payment,postgres,templates]; extra == "all"
|
|
46
|
+
Requires-Dist: velocity-python[aws,excel,http,payment,pdf,postgres,templates]; extra == "all"
|
|
45
47
|
Provides-Extra: dev
|
|
46
48
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
47
49
|
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
@@ -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.13"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Velocity Team", email="info@codeclubs.org" },
|
|
10
10
|
]
|
|
@@ -48,6 +48,9 @@ excel = [
|
|
|
48
48
|
templates = [
|
|
49
49
|
"jinja2>=3.1.0",
|
|
50
50
|
]
|
|
51
|
+
pdf = [
|
|
52
|
+
"weasyprint>=62.0",
|
|
53
|
+
]
|
|
51
54
|
http = [
|
|
52
55
|
"requests>=2.32.0",
|
|
53
56
|
]
|
|
@@ -65,7 +68,7 @@ payment = [
|
|
|
65
68
|
"braintree>=4.30.0",
|
|
66
69
|
]
|
|
67
70
|
all = [
|
|
68
|
-
"velocity-python[postgres,aws,excel,templates,http,payment]",
|
|
71
|
+
"velocity-python[postgres,aws,excel,templates,http,payment,pdf]",
|
|
69
72
|
]
|
|
70
73
|
dev = [
|
|
71
74
|
"pytest>=8.0.0",
|
|
@@ -100,14 +100,17 @@ class MigrationRunner:
|
|
|
100
100
|
"""
|
|
101
101
|
Discovers, applies, and rolls back schema migrations.
|
|
102
102
|
|
|
103
|
-
Migrations are tracked in a ``velocity_migrations`` table
|
|
103
|
+
Migrations are tracked in a ``velocity_migrations`` table managed by
|
|
104
|
+
velocity (with standard system columns like ``sys_id``, ``sys_created``,
|
|
105
|
+
etc.). User columns:
|
|
104
106
|
|
|
105
|
-
- ``version`` (INT
|
|
107
|
+
- ``version`` (INT) — migration version number
|
|
106
108
|
- ``description`` (TEXT) — human-readable description
|
|
107
|
-
- ``applied_at`` (TIMESTAMPTZ) — when the migration was applied
|
|
108
109
|
- ``checksum`` (TEXT) — SHA-256 of the migration function source
|
|
109
110
|
- ``execution_ms`` (INT) — how long the migration took in milliseconds
|
|
110
111
|
|
|
112
|
+
The ``sys_created`` system column serves as the applied-at timestamp.
|
|
113
|
+
|
|
111
114
|
Args:
|
|
112
115
|
engine: A velocity Engine instance.
|
|
113
116
|
migrations_dir: Optional path to a directory containing migration .py files.
|
|
@@ -145,43 +148,23 @@ class MigrationRunner:
|
|
|
145
148
|
"""Create the velocity_migrations tracking table if it doesn't exist."""
|
|
146
149
|
table = tx.table(TRACKING_TABLE)
|
|
147
150
|
if not table.exists():
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
elif server == 'SQL Server':
|
|
155
|
-
ts_col = "applied_at DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET()"
|
|
156
|
-
else:
|
|
157
|
-
ts_col = "applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
158
|
-
tx.execute(
|
|
159
|
-
f"""
|
|
160
|
-
CREATE TABLE IF NOT EXISTS {TRACKING_TABLE} (
|
|
161
|
-
version INTEGER PRIMARY KEY,
|
|
162
|
-
description TEXT NOT NULL DEFAULT '',
|
|
163
|
-
{ts_col},
|
|
164
|
-
checksum TEXT NOT NULL DEFAULT '',
|
|
165
|
-
execution_ms INTEGER NOT NULL DEFAULT 0
|
|
166
|
-
)
|
|
167
|
-
""",
|
|
168
|
-
cursor=tx.cursor(),
|
|
169
|
-
)
|
|
151
|
+
table.create(columns={
|
|
152
|
+
"version": int,
|
|
153
|
+
"description": str,
|
|
154
|
+
"checksum": str,
|
|
155
|
+
"execution_ms": int,
|
|
156
|
+
})
|
|
170
157
|
logger.info("Created tracking table: %s", TRACKING_TABLE)
|
|
171
158
|
|
|
172
159
|
def _get_applied_versions(self, tx) -> Dict[int, Dict[str, Any]]:
|
|
173
160
|
"""Return dict of {version: {description, applied_at, checksum, execution_ms}}."""
|
|
174
161
|
self._ensure_tracking_table(tx)
|
|
175
|
-
|
|
176
|
-
f"SELECT version, description, applied_at, checksum, execution_ms "
|
|
177
|
-
f"FROM {TRACKING_TABLE} ORDER BY version",
|
|
178
|
-
cursor=tx.cursor(),
|
|
179
|
-
)
|
|
162
|
+
table = tx.table(TRACKING_TABLE)
|
|
180
163
|
applied = {}
|
|
181
|
-
for row in
|
|
164
|
+
for row in table.select(orderby="version").all():
|
|
182
165
|
applied[row["version"]] = {
|
|
183
166
|
"description": row["description"],
|
|
184
|
-
"applied_at": row["
|
|
167
|
+
"applied_at": row["sys_created"],
|
|
185
168
|
"checksum": row["checksum"],
|
|
186
169
|
"execution_ms": row["execution_ms"],
|
|
187
170
|
}
|
|
@@ -189,20 +172,18 @@ class MigrationRunner:
|
|
|
189
172
|
|
|
190
173
|
def _record_migration(self, tx, version: int, description: str, checksum: str, execution_ms: int) -> None:
|
|
191
174
|
"""Insert a record into the tracking table."""
|
|
192
|
-
tx.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
175
|
+
table = tx.table(TRACKING_TABLE)
|
|
176
|
+
table.insert({
|
|
177
|
+
"version": version,
|
|
178
|
+
"description": description,
|
|
179
|
+
"checksum": checksum,
|
|
180
|
+
"execution_ms": execution_ms,
|
|
181
|
+
})
|
|
198
182
|
|
|
199
183
|
def _remove_migration_record(self, tx, version: int) -> None:
|
|
200
184
|
"""Delete a record from the tracking table."""
|
|
201
|
-
tx.
|
|
202
|
-
|
|
203
|
-
(version,),
|
|
204
|
-
cursor=tx.cursor(),
|
|
205
|
-
)
|
|
185
|
+
table = tx.table(TRACKING_TABLE)
|
|
186
|
+
table.delete(where={"version": version})
|
|
206
187
|
|
|
207
188
|
# ── Checksum ────────────────────────────────────────────────
|
|
208
189
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTML-to-PDF generation using WeasyPrint.
|
|
3
|
+
|
|
4
|
+
Provides a simple API for converting HTML strings to PDF bytes,
|
|
5
|
+
with support for page size, orientation, margins, and page footers.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from velocity.misc.pdf import html_to_pdf
|
|
10
|
+
|
|
11
|
+
# Simple conversion
|
|
12
|
+
pdf_bytes = html_to_pdf("<h1>Hello</h1><p>World</p>")
|
|
13
|
+
|
|
14
|
+
# With options
|
|
15
|
+
pdf_bytes = html_to_pdf(
|
|
16
|
+
html,
|
|
17
|
+
page_size="Letter",
|
|
18
|
+
orientation="landscape",
|
|
19
|
+
margin="0.5in",
|
|
20
|
+
footer_right="Page {page} of {pages}",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Wrap a body fragment with a full HTML document + default styles
|
|
24
|
+
from velocity.misc.pdf import wrap_html, html_to_pdf
|
|
25
|
+
|
|
26
|
+
html = wrap_html("My Report", "<p>Content here</p>")
|
|
27
|
+
pdf_bytes = html_to_pdf(html)
|
|
28
|
+
|
|
29
|
+
Requires the ``pdf`` extra::
|
|
30
|
+
|
|
31
|
+
pip install velocity-python[pdf]
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
import os
|
|
38
|
+
from typing import Optional
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger("velocity.misc.pdf")
|
|
41
|
+
|
|
42
|
+
# On AWS Lambda, fontconfig needs to know where fonts.conf lives.
|
|
43
|
+
# The py-lib-support layer ships fonts/ at /opt/fonts/.
|
|
44
|
+
if "AWS_LAMBDA_FUNCTION_NAME" in os.environ:
|
|
45
|
+
os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import weasyprint
|
|
49
|
+
except ImportError:
|
|
50
|
+
weasyprint = None # type: ignore[assignment]
|
|
51
|
+
|
|
52
|
+
_DEFAULT_BODY_CSS = """\
|
|
53
|
+
body {
|
|
54
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
55
|
+
font-size: 11pt;
|
|
56
|
+
line-height: 1.6;
|
|
57
|
+
color: #222;
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 24px;
|
|
60
|
+
}
|
|
61
|
+
h1 { font-size: 20px; margin-bottom: 16px; }
|
|
62
|
+
p { margin: 0 0 12px; line-height: 1.6; }
|
|
63
|
+
ol, ul { padding-left: 24px; }
|
|
64
|
+
li { margin-bottom: 8px; }
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_weasyprint():
|
|
69
|
+
if weasyprint is None:
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"weasyprint is required for PDF generation. "
|
|
72
|
+
"Install it with: pip install velocity-python[pdf]"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_page_css(
|
|
77
|
+
page_size: str = "Letter",
|
|
78
|
+
orientation: str = "portrait",
|
|
79
|
+
margin: Optional[str] = None,
|
|
80
|
+
margin_top: Optional[str] = None,
|
|
81
|
+
margin_right: Optional[str] = None,
|
|
82
|
+
margin_bottom: Optional[str] = None,
|
|
83
|
+
margin_left: Optional[str] = None,
|
|
84
|
+
footer_right: Optional[str] = None,
|
|
85
|
+
footer_center: Optional[str] = None,
|
|
86
|
+
footer_font_size: str = "8pt",
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Build a ``@page`` CSS rule from the given options."""
|
|
89
|
+
parts = [f"size: {page_size} {orientation};"]
|
|
90
|
+
|
|
91
|
+
if margin:
|
|
92
|
+
parts.append(f"margin: {margin};")
|
|
93
|
+
else:
|
|
94
|
+
mt = margin_top or "0.5in"
|
|
95
|
+
mr = margin_right or "0.5in"
|
|
96
|
+
mb = margin_bottom or "0.75in"
|
|
97
|
+
ml = margin_left or "0.5in"
|
|
98
|
+
parts.append(f"margin: {mt} {mr} {mb} {ml};")
|
|
99
|
+
|
|
100
|
+
footer_blocks = []
|
|
101
|
+
if footer_right:
|
|
102
|
+
content = footer_right.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
|
|
103
|
+
content = f'" {content} "'
|
|
104
|
+
footer_blocks.append(
|
|
105
|
+
f"@bottom-right {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
|
|
106
|
+
)
|
|
107
|
+
if footer_center:
|
|
108
|
+
content = footer_center.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
|
|
109
|
+
content = f'" {content} "'
|
|
110
|
+
footer_blocks.append(
|
|
111
|
+
f"@bottom-center {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
rule_body = "\n ".join(parts)
|
|
115
|
+
footer_body = "\n ".join(footer_blocks)
|
|
116
|
+
return f"@page {{\n {rule_body}\n {footer_body}\n}}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def html_to_pdf(
|
|
120
|
+
html: str,
|
|
121
|
+
*,
|
|
122
|
+
page_size: str = "Letter",
|
|
123
|
+
orientation: str = "portrait",
|
|
124
|
+
margin: Optional[str] = None,
|
|
125
|
+
margin_top: Optional[str] = None,
|
|
126
|
+
margin_right: Optional[str] = None,
|
|
127
|
+
margin_bottom: Optional[str] = None,
|
|
128
|
+
margin_left: Optional[str] = None,
|
|
129
|
+
footer_right: Optional[str] = None,
|
|
130
|
+
footer_center: Optional[str] = None,
|
|
131
|
+
footer_font_size: str = "8pt",
|
|
132
|
+
) -> bytes:
|
|
133
|
+
"""
|
|
134
|
+
Convert an HTML string to PDF bytes.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
html: Complete HTML document string.
|
|
138
|
+
page_size: CSS page size (e.g. "Letter", "A4"). Default "Letter".
|
|
139
|
+
orientation: "portrait" or "landscape". Default "portrait".
|
|
140
|
+
margin: Shorthand margin for all sides (e.g. "0.5in").
|
|
141
|
+
margin_top/right/bottom/left: Individual margins (override ``margin``).
|
|
142
|
+
footer_right: Right-aligned footer text. Use ``{page}`` and ``{pages}``
|
|
143
|
+
for page numbering (e.g. ``"Page {page} of {pages}"``).
|
|
144
|
+
footer_center: Center-aligned footer text. Same placeholders.
|
|
145
|
+
footer_font_size: CSS font size for footer. Default "8pt".
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
PDF file contents as bytes.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ImportError: If weasyprint is not installed.
|
|
152
|
+
"""
|
|
153
|
+
_require_weasyprint()
|
|
154
|
+
|
|
155
|
+
page_css = _build_page_css(
|
|
156
|
+
page_size=page_size,
|
|
157
|
+
orientation=orientation,
|
|
158
|
+
margin=margin,
|
|
159
|
+
margin_top=margin_top,
|
|
160
|
+
margin_right=margin_right,
|
|
161
|
+
margin_bottom=margin_bottom,
|
|
162
|
+
margin_left=margin_left,
|
|
163
|
+
footer_right=footer_right,
|
|
164
|
+
footer_center=footer_center,
|
|
165
|
+
footer_font_size=footer_font_size,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
css = weasyprint.CSS(string=page_css)
|
|
169
|
+
doc = weasyprint.HTML(string=html)
|
|
170
|
+
return doc.write_pdf(stylesheets=[css])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def wrap_html(title: str, body: str, css: str = "") -> str:
|
|
174
|
+
"""
|
|
175
|
+
Wrap an HTML body fragment in a full HTML document with default styles.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
title: Document ``<title>`` and ``<h1>`` heading.
|
|
179
|
+
body: HTML body content (fragment, not full document).
|
|
180
|
+
css: Additional CSS to include after the default body styles.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Complete HTML document string ready for :func:`html_to_pdf`.
|
|
184
|
+
"""
|
|
185
|
+
all_css = _DEFAULT_BODY_CSS + "\n" + css
|
|
186
|
+
return f"""<!DOCTYPE html>
|
|
187
|
+
<html>
|
|
188
|
+
<head>
|
|
189
|
+
<meta charset="utf-8">
|
|
190
|
+
<title>{title}</title>
|
|
191
|
+
<style>
|
|
192
|
+
{all_css}
|
|
193
|
+
</style>
|
|
194
|
+
</head>
|
|
195
|
+
<body>
|
|
196
|
+
<h1>{title}</h1>
|
|
197
|
+
{body}
|
|
198
|
+
</body>
|
|
199
|
+
</html>"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
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
|
|
@@ -29,6 +29,8 @@ Provides-Extra: excel
|
|
|
29
29
|
Requires-Dist: openpyxl>=3.1.0; extra == "excel"
|
|
30
30
|
Provides-Extra: templates
|
|
31
31
|
Requires-Dist: jinja2>=3.1.0; extra == "templates"
|
|
32
|
+
Provides-Extra: pdf
|
|
33
|
+
Requires-Dist: weasyprint>=62.0; extra == "pdf"
|
|
32
34
|
Provides-Extra: http
|
|
33
35
|
Requires-Dist: requests>=2.32.0; extra == "http"
|
|
34
36
|
Provides-Extra: mysql
|
|
@@ -41,7 +43,7 @@ Provides-Extra: payment
|
|
|
41
43
|
Requires-Dist: stripe>=12.0.0; extra == "payment"
|
|
42
44
|
Requires-Dist: braintree>=4.30.0; extra == "payment"
|
|
43
45
|
Provides-Extra: all
|
|
44
|
-
Requires-Dist: velocity-python[aws,excel,http,payment,postgres,templates]; extra == "all"
|
|
46
|
+
Requires-Dist: velocity-python[aws,excel,http,payment,pdf,postgres,templates]; extra == "all"
|
|
45
47
|
Provides-Extra: dev
|
|
46
48
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
47
49
|
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
@@ -121,6 +121,7 @@ src/velocity/misc/export.py
|
|
|
121
121
|
src/velocity/misc/format.py
|
|
122
122
|
src/velocity/misc/mail.py
|
|
123
123
|
src/velocity/misc/merge.py
|
|
124
|
+
src/velocity/misc/pdf.py
|
|
124
125
|
src/velocity/misc/timer.py
|
|
125
126
|
src/velocity/misc/tools.py
|
|
126
127
|
src/velocity/misc/conv/__init__.py
|
|
@@ -170,6 +171,7 @@ tests/test_payment_demo_profiles.py
|
|
|
170
171
|
tests/test_payment_profiles.py
|
|
171
172
|
tests/test_payment_router.py
|
|
172
173
|
tests/test_payment_stripe_adapter.py
|
|
174
|
+
tests/test_pdf.py
|
|
173
175
|
tests/test_prepared_statements.py
|
|
174
176
|
tests/test_psycopg3_upgrade.py
|
|
175
177
|
tests/test_query_cache.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
sqlparse>=0.5.0
|
|
2
2
|
|
|
3
3
|
[all]
|
|
4
|
-
velocity-python[aws,excel,http,payment,postgres,templates]
|
|
4
|
+
velocity-python[aws,excel,http,payment,pdf,postgres,templates]
|
|
5
5
|
|
|
6
6
|
[aws]
|
|
7
7
|
boto3>=1.35.0
|
|
@@ -33,6 +33,9 @@ mysql-connector-python>=9.0.0
|
|
|
33
33
|
stripe>=12.0.0
|
|
34
34
|
braintree>=4.30.0
|
|
35
35
|
|
|
36
|
+
[pdf]
|
|
37
|
+
weasyprint>=62.0
|
|
38
|
+
|
|
36
39
|
[postgres]
|
|
37
40
|
psycopg[binary]>=3.2.0
|
|
38
41
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Tests for velocity.misc.pdf — HTML-to-PDF generation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
from velocity.misc.pdf import (
|
|
7
|
+
html_to_pdf,
|
|
8
|
+
wrap_html,
|
|
9
|
+
_build_page_css,
|
|
10
|
+
_DEFAULT_BODY_CSS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── wrap_html ───────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestWrapHtml:
|
|
18
|
+
|
|
19
|
+
def test_produces_full_document(self):
|
|
20
|
+
result = wrap_html("My Title", "<p>Hello</p>")
|
|
21
|
+
assert "<!DOCTYPE html>" in result
|
|
22
|
+
assert "<title>My Title</title>" in result
|
|
23
|
+
assert "<h1>My Title</h1>" in result
|
|
24
|
+
assert "<p>Hello</p>" in result
|
|
25
|
+
|
|
26
|
+
def test_includes_default_css(self):
|
|
27
|
+
result = wrap_html("T", "<p>body</p>")
|
|
28
|
+
assert "font-family:" in result
|
|
29
|
+
assert _DEFAULT_BODY_CSS.strip()[:30] in result
|
|
30
|
+
|
|
31
|
+
def test_includes_custom_css(self):
|
|
32
|
+
result = wrap_html("T", "<p>body</p>", css=".custom { color: red; }")
|
|
33
|
+
assert ".custom { color: red; }" in result
|
|
34
|
+
|
|
35
|
+
def test_empty_body(self):
|
|
36
|
+
result = wrap_html("T", "")
|
|
37
|
+
assert "<h1>T</h1>" in result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── _build_page_css ─────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestBuildPageCss:
|
|
44
|
+
|
|
45
|
+
def test_defaults(self):
|
|
46
|
+
css = _build_page_css()
|
|
47
|
+
assert "size: Letter portrait;" in css
|
|
48
|
+
assert "margin:" in css
|
|
49
|
+
|
|
50
|
+
def test_landscape(self):
|
|
51
|
+
css = _build_page_css(orientation="landscape")
|
|
52
|
+
assert "size: Letter landscape;" in css
|
|
53
|
+
|
|
54
|
+
def test_a4(self):
|
|
55
|
+
css = _build_page_css(page_size="A4")
|
|
56
|
+
assert "size: A4 portrait;" in css
|
|
57
|
+
|
|
58
|
+
def test_shorthand_margin(self):
|
|
59
|
+
css = _build_page_css(margin="1in")
|
|
60
|
+
assert "margin: 1in;" in css
|
|
61
|
+
|
|
62
|
+
def test_individual_margins(self):
|
|
63
|
+
css = _build_page_css(
|
|
64
|
+
margin_top="0.5in",
|
|
65
|
+
margin_right="0.45in",
|
|
66
|
+
margin_bottom="0.55in",
|
|
67
|
+
margin_left="0.45in",
|
|
68
|
+
)
|
|
69
|
+
assert "margin: 0.5in 0.45in 0.55in 0.45in;" in css
|
|
70
|
+
|
|
71
|
+
def test_footer_right(self):
|
|
72
|
+
css = _build_page_css(footer_right="Page {page} of {pages}")
|
|
73
|
+
assert "@bottom-right" in css
|
|
74
|
+
assert 'counter(page)' in css
|
|
75
|
+
assert 'counter(pages)' in css
|
|
76
|
+
|
|
77
|
+
def test_footer_center(self):
|
|
78
|
+
css = _build_page_css(footer_center="CaringCent, LLC")
|
|
79
|
+
assert "@bottom-center" in css
|
|
80
|
+
assert "CaringCent, LLC" in css
|
|
81
|
+
|
|
82
|
+
def test_footer_font_size(self):
|
|
83
|
+
css = _build_page_css(footer_right="pg", footer_font_size="10pt")
|
|
84
|
+
assert "font-size: 10pt;" in css
|
|
85
|
+
|
|
86
|
+
def test_no_footers(self):
|
|
87
|
+
css = _build_page_css()
|
|
88
|
+
assert "@bottom-right" not in css
|
|
89
|
+
assert "@bottom-center" not in css
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── html_to_pdf ─────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestHtmlToPdf:
|
|
96
|
+
|
|
97
|
+
def test_raises_without_weasyprint(self):
|
|
98
|
+
with patch("velocity.misc.pdf.weasyprint", None):
|
|
99
|
+
with pytest.raises(ImportError, match="weasyprint is required"):
|
|
100
|
+
html_to_pdf("<h1>test</h1>")
|
|
101
|
+
|
|
102
|
+
def test_calls_weasyprint(self):
|
|
103
|
+
mock_wp = MagicMock()
|
|
104
|
+
mock_doc = MagicMock()
|
|
105
|
+
mock_doc.write_pdf.return_value = b"%PDF-fake"
|
|
106
|
+
mock_wp.HTML.return_value = mock_doc
|
|
107
|
+
mock_wp.CSS.return_value = MagicMock()
|
|
108
|
+
|
|
109
|
+
with patch("velocity.misc.pdf.weasyprint", mock_wp):
|
|
110
|
+
result = html_to_pdf("<h1>Hello</h1>")
|
|
111
|
+
|
|
112
|
+
assert result == b"%PDF-fake"
|
|
113
|
+
mock_wp.HTML.assert_called_once()
|
|
114
|
+
call_kwargs = mock_wp.HTML.call_args
|
|
115
|
+
assert "<h1>Hello</h1>" in call_kwargs[1]["string"]
|
|
116
|
+
mock_doc.write_pdf.assert_called_once()
|
|
117
|
+
|
|
118
|
+
def test_passes_page_css_as_stylesheet(self):
|
|
119
|
+
mock_wp = MagicMock()
|
|
120
|
+
mock_doc = MagicMock()
|
|
121
|
+
mock_doc.write_pdf.return_value = b"%PDF"
|
|
122
|
+
mock_wp.HTML.return_value = mock_doc
|
|
123
|
+
mock_css_obj = MagicMock()
|
|
124
|
+
mock_wp.CSS.return_value = mock_css_obj
|
|
125
|
+
|
|
126
|
+
with patch("velocity.misc.pdf.weasyprint", mock_wp):
|
|
127
|
+
html_to_pdf(
|
|
128
|
+
"<h1>test</h1>",
|
|
129
|
+
page_size="A4",
|
|
130
|
+
orientation="landscape",
|
|
131
|
+
footer_right="Page {page}",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# CSS should contain @page with landscape A4
|
|
135
|
+
css_string = mock_wp.CSS.call_args[1]["string"]
|
|
136
|
+
assert "A4 landscape" in css_string
|
|
137
|
+
assert "@bottom-right" in css_string
|
|
138
|
+
|
|
139
|
+
# write_pdf should receive the CSS object
|
|
140
|
+
mock_doc.write_pdf.assert_called_once_with(stylesheets=[mock_css_obj])
|
|
141
|
+
|
|
142
|
+
def test_landscape_with_margins(self):
|
|
143
|
+
mock_wp = MagicMock()
|
|
144
|
+
mock_doc = MagicMock()
|
|
145
|
+
mock_doc.write_pdf.return_value = b"%PDF"
|
|
146
|
+
mock_wp.HTML.return_value = mock_doc
|
|
147
|
+
mock_wp.CSS.return_value = MagicMock()
|
|
148
|
+
|
|
149
|
+
with patch("velocity.misc.pdf.weasyprint", mock_wp):
|
|
150
|
+
html_to_pdf(
|
|
151
|
+
"<h1>report</h1>",
|
|
152
|
+
orientation="landscape",
|
|
153
|
+
margin_top="0.5in",
|
|
154
|
+
margin_right="0.45in",
|
|
155
|
+
margin_bottom="0.55in",
|
|
156
|
+
margin_left="0.45in",
|
|
157
|
+
footer_right="Page {page} of {pages}",
|
|
158
|
+
footer_font_size="8pt",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
css_string = mock_wp.CSS.call_args[1]["string"]
|
|
162
|
+
assert "landscape" in css_string
|
|
163
|
+
assert "0.5in 0.45in 0.55in 0.45in" in css_string
|
|
164
|
+
assert "counter(page)" in css_string
|
|
165
|
+
assert "counter(pages)" in css_string
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ── Integration with wrap_html + html_to_pdf ────────────────────────
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestIntegration:
|
|
172
|
+
|
|
173
|
+
def test_wrap_then_convert(self):
|
|
174
|
+
mock_wp = MagicMock()
|
|
175
|
+
mock_doc = MagicMock()
|
|
176
|
+
mock_doc.write_pdf.return_value = b"%PDF-wrapped"
|
|
177
|
+
mock_wp.HTML.return_value = mock_doc
|
|
178
|
+
mock_wp.CSS.return_value = MagicMock()
|
|
179
|
+
|
|
180
|
+
html = wrap_html("Report", "<p>Content</p>")
|
|
181
|
+
|
|
182
|
+
with patch("velocity.misc.pdf.weasyprint", mock_wp):
|
|
183
|
+
result = html_to_pdf(html)
|
|
184
|
+
|
|
185
|
+
assert result == b"%PDF-wrapped"
|
|
186
|
+
passed_html = mock_wp.HTML.call_args[1]["string"]
|
|
187
|
+
assert "<title>Report</title>" in passed_html
|
|
188
|
+
assert "<p>Content</p>" in passed_html
|