velocity-python 0.1.18__tar.gz → 0.1.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {velocity_python-0.1.18/src/velocity_python.egg-info → velocity_python-0.1.20}/PKG-INFO +2 -2
- {velocity_python-0.1.18 → velocity_python-0.1.20}/pyproject.toml +2 -2
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.20/src/velocity/misc/pdf.py +217 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20/src/velocity_python.egg-info}/PKG-INFO +2 -2
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/requires.txt +1 -1
- velocity_python-0.1.20/tests/test_pdf.py +154 -0
- velocity_python-0.1.18/src/velocity/misc/pdf.py +0 -201
- velocity_python-0.1.18/tests/test_pdf.py +0 -188
- {velocity_python-0.1.18 → velocity_python-0.1.20}/LICENSE +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/README.md +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/setup.cfg +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_observability.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.18 → velocity_python-0.1.20}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.20
|
|
4
4
|
Summary: A rapid application development library for interfacing with data storage
|
|
5
5
|
Author-email: Velocity Team <info@codeclubs.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,7 +30,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == "excel"
|
|
|
30
30
|
Provides-Extra: templates
|
|
31
31
|
Requires-Dist: jinja2>=3.1.0; extra == "templates"
|
|
32
32
|
Provides-Extra: pdf
|
|
33
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
|
|
34
34
|
Provides-Extra: http
|
|
35
35
|
Requires-Dist: requests>=2.32.0; extra == "http"
|
|
36
36
|
Provides-Extra: mysql
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "velocity-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.20"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Velocity Team", email="info@codeclubs.org" },
|
|
10
10
|
]
|
|
@@ -49,7 +49,7 @@ templates = [
|
|
|
49
49
|
"jinja2>=3.1.0",
|
|
50
50
|
]
|
|
51
51
|
pdf = [
|
|
52
|
-
"
|
|
52
|
+
"pdfkit>=1.0.0",
|
|
53
53
|
]
|
|
54
54
|
http = [
|
|
55
55
|
"requests>=2.32.0",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTML-to-PDF generation using pdfkit + wkhtmltopdf.
|
|
3
|
+
|
|
4
|
+
The Python dependency surface is intentionally small: pdfkit wraps an external
|
|
5
|
+
``wkhtmltopdf`` binary. On Lambda, the binary is typically supplied by the
|
|
6
|
+
support layer at ``/opt/bin/wkhtmltopdf`` with companion shared libraries in
|
|
7
|
+
``/opt/lib``.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from velocity.misc.pdf import html_to_pdf
|
|
12
|
+
|
|
13
|
+
pdf_bytes = html_to_pdf("<h1>Hello</h1><p>World</p>")
|
|
14
|
+
|
|
15
|
+
pdf_bytes = html_to_pdf(
|
|
16
|
+
html,
|
|
17
|
+
page_size="Letter",
|
|
18
|
+
orientation="landscape",
|
|
19
|
+
margin="0.5in",
|
|
20
|
+
footer_right="Page {page} of {pages}",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from velocity.misc.pdf import wrap_html, html_to_pdf
|
|
24
|
+
|
|
25
|
+
html = wrap_html("My Report", "<p>Content here</p>")
|
|
26
|
+
pdf_bytes = html_to_pdf(html)
|
|
27
|
+
|
|
28
|
+
Requires the ``pdf`` extra plus a runnable ``wkhtmltopdf`` binary::
|
|
29
|
+
|
|
30
|
+
pip install velocity-python[pdf]
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import shutil
|
|
38
|
+
from typing import Optional
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger("velocity.misc.pdf")
|
|
41
|
+
|
|
42
|
+
_pdfkit_import_error: Exception | None = None
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import pdfkit
|
|
46
|
+
except ImportError as exc:
|
|
47
|
+
_pdfkit_import_error = exc
|
|
48
|
+
pdfkit = None # type: ignore[assignment]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_DEFAULT_BODY_CSS = """\
|
|
52
|
+
body {
|
|
53
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
54
|
+
font-size: 11pt;
|
|
55
|
+
line-height: 1.6;
|
|
56
|
+
color: #222;
|
|
57
|
+
margin: 0;
|
|
58
|
+
padding: 24px;
|
|
59
|
+
}
|
|
60
|
+
h1 { font-size: 20px; margin-bottom: 16px; }
|
|
61
|
+
p { margin: 0 0 12px; line-height: 1.6; }
|
|
62
|
+
ol, ul { padding-left: 24px; }
|
|
63
|
+
li { margin-bottom: 8px; }
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _prepend_env_path(name: str, *paths: str) -> None:
|
|
68
|
+
current = os.environ.get(name, "")
|
|
69
|
+
existing = [path for path in current.split(":") if path]
|
|
70
|
+
merged: list[str] = []
|
|
71
|
+
|
|
72
|
+
for path in paths:
|
|
73
|
+
if path and os.path.isdir(path) and path not in merged:
|
|
74
|
+
merged.append(path)
|
|
75
|
+
|
|
76
|
+
for path in existing:
|
|
77
|
+
if path not in merged:
|
|
78
|
+
merged.append(path)
|
|
79
|
+
|
|
80
|
+
if merged:
|
|
81
|
+
os.environ[name] = ":".join(merged)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _configure_wkhtml_runtime() -> None:
|
|
85
|
+
if "AWS_LAMBDA_FUNCTION_NAME" not in os.environ:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")
|
|
89
|
+
os.environ.setdefault("HOME", "/tmp")
|
|
90
|
+
os.environ.setdefault("XDG_RUNTIME_DIR", "/tmp")
|
|
91
|
+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
92
|
+
_prepend_env_path("LD_LIBRARY_PATH", "/opt/lib")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _find_wkhtmltopdf() -> str | None:
|
|
96
|
+
configured = os.environ.get("WKHTMLTOPDF_PATH")
|
|
97
|
+
candidates = [
|
|
98
|
+
configured,
|
|
99
|
+
shutil.which("wkhtmltopdf"),
|
|
100
|
+
"/opt/bin/wkhtmltopdf",
|
|
101
|
+
"/opt/wkhtmltopdf",
|
|
102
|
+
"/opt/python/bin/wkhtmltopdf",
|
|
103
|
+
"/var/task/bin/wkhtmltopdf",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for candidate in candidates:
|
|
107
|
+
if candidate and os.path.exists(candidate) and os.access(candidate, os.X_OK):
|
|
108
|
+
return candidate
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _normalize_footer_text(text: str) -> str:
|
|
113
|
+
return text.replace("{page}", "[page]").replace("{pages}", "[topage]")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _normalize_footer_font_size(value: str) -> str:
|
|
117
|
+
normalized = str(value or "8").strip().lower()
|
|
118
|
+
if normalized.endswith("pt"):
|
|
119
|
+
normalized = normalized[:-2].strip()
|
|
120
|
+
return normalized or "8"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _require_pdf_backend() -> tuple[object, object]:
|
|
124
|
+
if pdfkit is None:
|
|
125
|
+
base_message = (
|
|
126
|
+
"pdfkit is required for PDF generation. "
|
|
127
|
+
"Install it with: pip install velocity-python[pdf] or pip install pdfkit"
|
|
128
|
+
)
|
|
129
|
+
if _pdfkit_import_error is not None:
|
|
130
|
+
raise ImportError(
|
|
131
|
+
f"{base_message} Root cause: {_pdfkit_import_error!r}"
|
|
132
|
+
) from _pdfkit_import_error
|
|
133
|
+
raise ImportError(base_message)
|
|
134
|
+
|
|
135
|
+
_configure_wkhtml_runtime()
|
|
136
|
+
wkhtmltopdf_path = _find_wkhtmltopdf()
|
|
137
|
+
if not wkhtmltopdf_path:
|
|
138
|
+
raise ImportError(
|
|
139
|
+
"wkhtmltopdf binary not available for PDF generation. "
|
|
140
|
+
"Expected it in PATH, WKHTMLTOPDF_PATH, or the Lambda layer under /opt/bin."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return pdfkit, pdfkit.configuration(wkhtmltopdf=wkhtmltopdf_path)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def html_to_pdf(
|
|
147
|
+
html: str,
|
|
148
|
+
*,
|
|
149
|
+
page_size: str = "Letter",
|
|
150
|
+
orientation: str = "portrait",
|
|
151
|
+
margin: Optional[str] = None,
|
|
152
|
+
margin_top: Optional[str] = None,
|
|
153
|
+
margin_right: Optional[str] = None,
|
|
154
|
+
margin_bottom: Optional[str] = None,
|
|
155
|
+
margin_left: Optional[str] = None,
|
|
156
|
+
footer_right: Optional[str] = None,
|
|
157
|
+
footer_center: Optional[str] = None,
|
|
158
|
+
footer_font_size: str = "8pt",
|
|
159
|
+
) -> bytes:
|
|
160
|
+
"""Convert an HTML string to PDF bytes."""
|
|
161
|
+
pdf_backend, configuration = _require_pdf_backend()
|
|
162
|
+
|
|
163
|
+
options: dict[str, str] = {
|
|
164
|
+
"encoding": "UTF-8",
|
|
165
|
+
"page-size": page_size,
|
|
166
|
+
"orientation": orientation.capitalize(),
|
|
167
|
+
"enable-local-file-access": "",
|
|
168
|
+
"quiet": "",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if margin:
|
|
172
|
+
options["margin-top"] = margin
|
|
173
|
+
options["margin-right"] = margin
|
|
174
|
+
options["margin-bottom"] = margin
|
|
175
|
+
options["margin-left"] = margin
|
|
176
|
+
else:
|
|
177
|
+
options["margin-top"] = margin_top or "0.5in"
|
|
178
|
+
options["margin-right"] = margin_right or "0.5in"
|
|
179
|
+
options["margin-bottom"] = margin_bottom or "0.75in"
|
|
180
|
+
options["margin-left"] = margin_left or "0.5in"
|
|
181
|
+
|
|
182
|
+
if footer_right:
|
|
183
|
+
options["footer-right"] = _normalize_footer_text(footer_right)
|
|
184
|
+
if footer_center:
|
|
185
|
+
options["footer-center"] = _normalize_footer_text(footer_center)
|
|
186
|
+
if footer_right or footer_center:
|
|
187
|
+
options["footer-font-size"] = _normalize_footer_font_size(footer_font_size)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
return pdf_backend.from_string(
|
|
191
|
+
html,
|
|
192
|
+
False,
|
|
193
|
+
configuration=configuration,
|
|
194
|
+
options=options,
|
|
195
|
+
)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
logger.exception("wkhtmltopdf PDF generation failed")
|
|
198
|
+
raise RuntimeError(f"PDF generation failed: {exc}") from exc
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def wrap_html(title: str, body: str, css: str = "") -> str:
|
|
202
|
+
"""Wrap a body fragment in a complete HTML document with default styles."""
|
|
203
|
+
all_css = _DEFAULT_BODY_CSS + "\n" + css
|
|
204
|
+
return f"""<!DOCTYPE html>
|
|
205
|
+
<html>
|
|
206
|
+
<head>
|
|
207
|
+
<meta charset=\"utf-8\">
|
|
208
|
+
<title>{title}</title>
|
|
209
|
+
<style>
|
|
210
|
+
{all_css}
|
|
211
|
+
</style>
|
|
212
|
+
</head>
|
|
213
|
+
<body>
|
|
214
|
+
<h1>{title}</h1>
|
|
215
|
+
{body}
|
|
216
|
+
</body>
|
|
217
|
+
</html>"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.20
|
|
4
4
|
Summary: A rapid application development library for interfacing with data storage
|
|
5
5
|
Author-email: Velocity Team <info@codeclubs.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,7 +30,7 @@ Requires-Dist: openpyxl>=3.1.0; extra == "excel"
|
|
|
30
30
|
Provides-Extra: templates
|
|
31
31
|
Requires-Dist: jinja2>=3.1.0; extra == "templates"
|
|
32
32
|
Provides-Extra: pdf
|
|
33
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: pdfkit>=1.0.0; extra == "pdf"
|
|
34
34
|
Provides-Extra: http
|
|
35
35
|
Requires-Dist: requests>=2.32.0; extra == "http"
|
|
36
36
|
Provides-Extra: mysql
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Tests for velocity.misc.pdf — HTML-to-PDF generation."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from velocity.misc.pdf import _DEFAULT_BODY_CSS, html_to_pdf, wrap_html
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestWrapHtml:
|
|
11
|
+
|
|
12
|
+
def test_produces_full_document(self):
|
|
13
|
+
result = wrap_html("My Title", "<p>Hello</p>")
|
|
14
|
+
assert "<!DOCTYPE html>" in result
|
|
15
|
+
assert "<title>My Title</title>" in result
|
|
16
|
+
assert "<h1>My Title</h1>" in result
|
|
17
|
+
assert "<p>Hello</p>" in result
|
|
18
|
+
|
|
19
|
+
def test_includes_default_css(self):
|
|
20
|
+
result = wrap_html("T", "<p>body</p>")
|
|
21
|
+
assert "font-family:" in result
|
|
22
|
+
assert _DEFAULT_BODY_CSS.strip()[:30] in result
|
|
23
|
+
|
|
24
|
+
def test_includes_custom_css(self):
|
|
25
|
+
result = wrap_html("T", "<p>body</p>", css=".custom { color: red; }")
|
|
26
|
+
assert ".custom { color: red; }" in result
|
|
27
|
+
|
|
28
|
+
def test_empty_body(self):
|
|
29
|
+
result = wrap_html("T", "")
|
|
30
|
+
assert "<h1>T</h1>" in result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestHtmlToPdf:
|
|
34
|
+
|
|
35
|
+
def test_raises_without_pdfkit(self):
|
|
36
|
+
with patch("velocity.misc.pdf.pdfkit", None), patch(
|
|
37
|
+
"velocity.misc.pdf._pdfkit_import_error", ImportError("missing pdfkit")
|
|
38
|
+
):
|
|
39
|
+
with pytest.raises(ImportError, match="pdfkit is required"):
|
|
40
|
+
html_to_pdf("<h1>test</h1>")
|
|
41
|
+
|
|
42
|
+
def test_raises_without_wkhtmltopdf_binary(self):
|
|
43
|
+
mock_pdfkit = MagicMock()
|
|
44
|
+
|
|
45
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
46
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value=None
|
|
47
|
+
):
|
|
48
|
+
with pytest.raises(ImportError, match="wkhtmltopdf binary not available"):
|
|
49
|
+
html_to_pdf("<h1>test</h1>")
|
|
50
|
+
|
|
51
|
+
def test_calls_pdfkit_with_defaults(self):
|
|
52
|
+
mock_pdfkit = MagicMock()
|
|
53
|
+
mock_config = object()
|
|
54
|
+
mock_pdfkit.configuration.return_value = mock_config
|
|
55
|
+
mock_pdfkit.from_string.return_value = b"%PDF-fake"
|
|
56
|
+
|
|
57
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
58
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
|
|
59
|
+
):
|
|
60
|
+
result = html_to_pdf("<h1>Hello</h1>")
|
|
61
|
+
|
|
62
|
+
assert result == b"%PDF-fake"
|
|
63
|
+
mock_pdfkit.configuration.assert_called_once_with(
|
|
64
|
+
wkhtmltopdf="/opt/bin/wkhtmltopdf"
|
|
65
|
+
)
|
|
66
|
+
mock_pdfkit.from_string.assert_called_once()
|
|
67
|
+
call_args = mock_pdfkit.from_string.call_args
|
|
68
|
+
assert call_args.args[0] == "<h1>Hello</h1>"
|
|
69
|
+
assert call_args.args[1] is False
|
|
70
|
+
assert call_args.kwargs["configuration"] is mock_config
|
|
71
|
+
options = call_args.kwargs["options"]
|
|
72
|
+
assert options["page-size"] == "Letter"
|
|
73
|
+
assert options["orientation"] == "Portrait"
|
|
74
|
+
assert options["margin-top"] == "0.5in"
|
|
75
|
+
assert options["margin-right"] == "0.5in"
|
|
76
|
+
assert options["margin-bottom"] == "0.75in"
|
|
77
|
+
assert options["margin-left"] == "0.5in"
|
|
78
|
+
assert options["enable-local-file-access"] == ""
|
|
79
|
+
|
|
80
|
+
def test_passes_footer_and_margin_options(self):
|
|
81
|
+
mock_pdfkit = MagicMock()
|
|
82
|
+
mock_pdfkit.configuration.return_value = object()
|
|
83
|
+
mock_pdfkit.from_string.return_value = b"%PDF"
|
|
84
|
+
|
|
85
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
86
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
|
|
87
|
+
):
|
|
88
|
+
html_to_pdf(
|
|
89
|
+
"<h1>report</h1>",
|
|
90
|
+
page_size="A4",
|
|
91
|
+
orientation="landscape",
|
|
92
|
+
margin_top="0.5in",
|
|
93
|
+
margin_right="0.45in",
|
|
94
|
+
margin_bottom="0.55in",
|
|
95
|
+
margin_left="0.45in",
|
|
96
|
+
footer_right="Page {page} of {pages}",
|
|
97
|
+
footer_center="CaringCent, LLC",
|
|
98
|
+
footer_font_size="8pt",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
options = mock_pdfkit.from_string.call_args.kwargs["options"]
|
|
102
|
+
assert options["page-size"] == "A4"
|
|
103
|
+
assert options["orientation"] == "Landscape"
|
|
104
|
+
assert options["margin-top"] == "0.5in"
|
|
105
|
+
assert options["margin-right"] == "0.45in"
|
|
106
|
+
assert options["margin-bottom"] == "0.55in"
|
|
107
|
+
assert options["margin-left"] == "0.45in"
|
|
108
|
+
assert options["footer-right"] == "Page [page] of [topage]"
|
|
109
|
+
assert options["footer-center"] == "CaringCent, LLC"
|
|
110
|
+
assert options["footer-font-size"] == "8"
|
|
111
|
+
|
|
112
|
+
def test_shorthand_margin_applies_to_all_sides(self):
|
|
113
|
+
mock_pdfkit = MagicMock()
|
|
114
|
+
mock_pdfkit.configuration.return_value = object()
|
|
115
|
+
mock_pdfkit.from_string.return_value = b"%PDF"
|
|
116
|
+
|
|
117
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
118
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
|
|
119
|
+
):
|
|
120
|
+
html_to_pdf("<h1>report</h1>", margin="1in", footer_font_size="10pt")
|
|
121
|
+
|
|
122
|
+
options = mock_pdfkit.from_string.call_args.kwargs["options"]
|
|
123
|
+
assert options["margin-top"] == "1in"
|
|
124
|
+
assert options["margin-right"] == "1in"
|
|
125
|
+
assert options["margin-bottom"] == "1in"
|
|
126
|
+
assert options["margin-left"] == "1in"
|
|
127
|
+
|
|
128
|
+
def test_wrap_then_convert(self):
|
|
129
|
+
mock_pdfkit = MagicMock()
|
|
130
|
+
mock_pdfkit.configuration.return_value = object()
|
|
131
|
+
mock_pdfkit.from_string.return_value = b"%PDF-wrapped"
|
|
132
|
+
|
|
133
|
+
html = wrap_html("Report", "<p>Content</p>")
|
|
134
|
+
|
|
135
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
136
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
|
|
137
|
+
):
|
|
138
|
+
result = html_to_pdf(html)
|
|
139
|
+
|
|
140
|
+
assert result == b"%PDF-wrapped"
|
|
141
|
+
passed_html = mock_pdfkit.from_string.call_args.args[0]
|
|
142
|
+
assert "<title>Report</title>" in passed_html
|
|
143
|
+
assert "<p>Content</p>" in passed_html
|
|
144
|
+
|
|
145
|
+
def test_wraps_backend_errors(self):
|
|
146
|
+
mock_pdfkit = MagicMock()
|
|
147
|
+
mock_pdfkit.configuration.return_value = object()
|
|
148
|
+
mock_pdfkit.from_string.side_effect = OSError("boom")
|
|
149
|
+
|
|
150
|
+
with patch("velocity.misc.pdf.pdfkit", mock_pdfkit), patch(
|
|
151
|
+
"velocity.misc.pdf._find_wkhtmltopdf", return_value="/opt/bin/wkhtmltopdf"
|
|
152
|
+
):
|
|
153
|
+
with pytest.raises(RuntimeError, match="PDF generation failed: boom"):
|
|
154
|
+
html_to_pdf("<h1>test</h1>")
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HTML-to-PDF generation using WeasyPrint.
|
|
3
|
-
|
|
4
|
-
Provides a simple API for converting HTML strings to PDF bytes,
|
|
5
|
-
with support for page size, orientation, margins, and page footers.
|
|
6
|
-
|
|
7
|
-
Usage::
|
|
8
|
-
|
|
9
|
-
from velocity.misc.pdf import html_to_pdf
|
|
10
|
-
|
|
11
|
-
# Simple conversion
|
|
12
|
-
pdf_bytes = html_to_pdf("<h1>Hello</h1><p>World</p>")
|
|
13
|
-
|
|
14
|
-
# With options
|
|
15
|
-
pdf_bytes = html_to_pdf(
|
|
16
|
-
html,
|
|
17
|
-
page_size="Letter",
|
|
18
|
-
orientation="landscape",
|
|
19
|
-
margin="0.5in",
|
|
20
|
-
footer_right="Page {page} of {pages}",
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
# Wrap a body fragment with a full HTML document + default styles
|
|
24
|
-
from velocity.misc.pdf import wrap_html, html_to_pdf
|
|
25
|
-
|
|
26
|
-
html = wrap_html("My Report", "<p>Content here</p>")
|
|
27
|
-
pdf_bytes = html_to_pdf(html)
|
|
28
|
-
|
|
29
|
-
Requires the ``pdf`` extra::
|
|
30
|
-
|
|
31
|
-
pip install velocity-python[pdf]
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
from __future__ import annotations
|
|
35
|
-
|
|
36
|
-
import logging
|
|
37
|
-
import os
|
|
38
|
-
from typing import Optional
|
|
39
|
-
|
|
40
|
-
logger = logging.getLogger("velocity.misc.pdf")
|
|
41
|
-
|
|
42
|
-
# On AWS Lambda, fontconfig needs to know where fonts.conf lives.
|
|
43
|
-
# The py-lib-support layer ships fonts/ at /opt/fonts/.
|
|
44
|
-
if "AWS_LAMBDA_FUNCTION_NAME" in os.environ:
|
|
45
|
-
os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
import weasyprint
|
|
49
|
-
except (ImportError, OSError):
|
|
50
|
-
# OSError: WeasyPrint's ffi.py raises OSError when native libs
|
|
51
|
-
# (libgobject, libpango, etc.) cannot be loaded via cffi.dlopen().
|
|
52
|
-
weasyprint = None # type: ignore[assignment]
|
|
53
|
-
|
|
54
|
-
_DEFAULT_BODY_CSS = """\
|
|
55
|
-
body {
|
|
56
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
57
|
-
font-size: 11pt;
|
|
58
|
-
line-height: 1.6;
|
|
59
|
-
color: #222;
|
|
60
|
-
margin: 0;
|
|
61
|
-
padding: 24px;
|
|
62
|
-
}
|
|
63
|
-
h1 { font-size: 20px; margin-bottom: 16px; }
|
|
64
|
-
p { margin: 0 0 12px; line-height: 1.6; }
|
|
65
|
-
ol, ul { padding-left: 24px; }
|
|
66
|
-
li { margin-bottom: 8px; }
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _require_weasyprint():
|
|
71
|
-
if weasyprint is None:
|
|
72
|
-
raise ImportError(
|
|
73
|
-
"weasyprint is required for PDF generation. "
|
|
74
|
-
"Install it with: pip install velocity-python[pdf]"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _build_page_css(
|
|
79
|
-
page_size: str = "Letter",
|
|
80
|
-
orientation: str = "portrait",
|
|
81
|
-
margin: Optional[str] = None,
|
|
82
|
-
margin_top: Optional[str] = None,
|
|
83
|
-
margin_right: Optional[str] = None,
|
|
84
|
-
margin_bottom: Optional[str] = None,
|
|
85
|
-
margin_left: Optional[str] = None,
|
|
86
|
-
footer_right: Optional[str] = None,
|
|
87
|
-
footer_center: Optional[str] = None,
|
|
88
|
-
footer_font_size: str = "8pt",
|
|
89
|
-
) -> str:
|
|
90
|
-
"""Build a ``@page`` CSS rule from the given options."""
|
|
91
|
-
parts = [f"size: {page_size} {orientation};"]
|
|
92
|
-
|
|
93
|
-
if margin:
|
|
94
|
-
parts.append(f"margin: {margin};")
|
|
95
|
-
else:
|
|
96
|
-
mt = margin_top or "0.5in"
|
|
97
|
-
mr = margin_right or "0.5in"
|
|
98
|
-
mb = margin_bottom or "0.75in"
|
|
99
|
-
ml = margin_left or "0.5in"
|
|
100
|
-
parts.append(f"margin: {mt} {mr} {mb} {ml};")
|
|
101
|
-
|
|
102
|
-
footer_blocks = []
|
|
103
|
-
if footer_right:
|
|
104
|
-
content = footer_right.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
|
|
105
|
-
content = f'" {content} "'
|
|
106
|
-
footer_blocks.append(
|
|
107
|
-
f"@bottom-right {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
|
|
108
|
-
)
|
|
109
|
-
if footer_center:
|
|
110
|
-
content = footer_center.replace("{page}", '" counter(page) "').replace("{pages}", '" counter(pages) "')
|
|
111
|
-
content = f'" {content} "'
|
|
112
|
-
footer_blocks.append(
|
|
113
|
-
f"@bottom-center {{ content: {content}; font-size: {footer_font_size}; color: #666; }}"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
rule_body = "\n ".join(parts)
|
|
117
|
-
footer_body = "\n ".join(footer_blocks)
|
|
118
|
-
return f"@page {{\n {rule_body}\n {footer_body}\n}}"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def html_to_pdf(
|
|
122
|
-
html: str,
|
|
123
|
-
*,
|
|
124
|
-
page_size: str = "Letter",
|
|
125
|
-
orientation: str = "portrait",
|
|
126
|
-
margin: Optional[str] = None,
|
|
127
|
-
margin_top: Optional[str] = None,
|
|
128
|
-
margin_right: Optional[str] = None,
|
|
129
|
-
margin_bottom: Optional[str] = None,
|
|
130
|
-
margin_left: Optional[str] = None,
|
|
131
|
-
footer_right: Optional[str] = None,
|
|
132
|
-
footer_center: Optional[str] = None,
|
|
133
|
-
footer_font_size: str = "8pt",
|
|
134
|
-
) -> bytes:
|
|
135
|
-
"""
|
|
136
|
-
Convert an HTML string to PDF bytes.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
html: Complete HTML document string.
|
|
140
|
-
page_size: CSS page size (e.g. "Letter", "A4"). Default "Letter".
|
|
141
|
-
orientation: "portrait" or "landscape". Default "portrait".
|
|
142
|
-
margin: Shorthand margin for all sides (e.g. "0.5in").
|
|
143
|
-
margin_top/right/bottom/left: Individual margins (override ``margin``).
|
|
144
|
-
footer_right: Right-aligned footer text. Use ``{page}`` and ``{pages}``
|
|
145
|
-
for page numbering (e.g. ``"Page {page} of {pages}"``).
|
|
146
|
-
footer_center: Center-aligned footer text. Same placeholders.
|
|
147
|
-
footer_font_size: CSS font size for footer. Default "8pt".
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
PDF file contents as bytes.
|
|
151
|
-
|
|
152
|
-
Raises:
|
|
153
|
-
ImportError: If weasyprint is not installed.
|
|
154
|
-
"""
|
|
155
|
-
_require_weasyprint()
|
|
156
|
-
|
|
157
|
-
page_css = _build_page_css(
|
|
158
|
-
page_size=page_size,
|
|
159
|
-
orientation=orientation,
|
|
160
|
-
margin=margin,
|
|
161
|
-
margin_top=margin_top,
|
|
162
|
-
margin_right=margin_right,
|
|
163
|
-
margin_bottom=margin_bottom,
|
|
164
|
-
margin_left=margin_left,
|
|
165
|
-
footer_right=footer_right,
|
|
166
|
-
footer_center=footer_center,
|
|
167
|
-
footer_font_size=footer_font_size,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
css = weasyprint.CSS(string=page_css)
|
|
171
|
-
doc = weasyprint.HTML(string=html)
|
|
172
|
-
return doc.write_pdf(stylesheets=[css])
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def wrap_html(title: str, body: str, css: str = "") -> str:
|
|
176
|
-
"""
|
|
177
|
-
Wrap an HTML body fragment in a full HTML document with default styles.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
title: Document ``<title>`` and ``<h1>`` heading.
|
|
181
|
-
body: HTML body content (fragment, not full document).
|
|
182
|
-
css: Additional CSS to include after the default body styles.
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
Complete HTML document string ready for :func:`html_to_pdf`.
|
|
186
|
-
"""
|
|
187
|
-
all_css = _DEFAULT_BODY_CSS + "\n" + css
|
|
188
|
-
return f"""<!DOCTYPE html>
|
|
189
|
-
<html>
|
|
190
|
-
<head>
|
|
191
|
-
<meta charset="utf-8">
|
|
192
|
-
<title>{title}</title>
|
|
193
|
-
<style>
|
|
194
|
-
{all_css}
|
|
195
|
-
</style>
|
|
196
|
-
</head>
|
|
197
|
-
<body>
|
|
198
|
-
<h1>{title}</h1>
|
|
199
|
-
{body}
|
|
200
|
-
</body>
|
|
201
|
-
</html>"""
|