velocity-python 0.1.17__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.17/src/velocity_python.egg-info → velocity_python-0.1.20}/PKG-INFO +2 -2
- {velocity_python-0.1.17 → velocity_python-0.1.20}/pyproject.toml +2 -2
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/base_handler.py +10 -9
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/exceptions.py +6 -1
- velocity_python-0.1.20/src/velocity/misc/pdf.py +217 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20/src/velocity_python.egg-info}/PKG-INFO +2 -2
- {velocity_python-0.1.17 → 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.17/src/velocity/misc/pdf.py +0 -201
- velocity_python-0.1.17/tests/test_pdf.py +0 -188
- {velocity_python-0.1.17 → velocity_python-0.1.20}/LICENSE +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/README.md +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/setup.cfg +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_observability.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.17 → velocity_python-0.1.20}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.17 → 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",
|
|
@@ -239,15 +239,16 @@ class BaseHandler:
|
|
|
239
239
|
or "error"
|
|
240
240
|
)
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
if getattr(exception, "log_to_cloudwatch", False):
|
|
243
|
+
logger.warning(
|
|
244
|
+
"AlertError during action execution",
|
|
245
|
+
exc_info=True,
|
|
246
|
+
extra={
|
|
247
|
+
"handler": self.__class__.__name__,
|
|
248
|
+
"action": getattr(local_context, "action", lambda: None)(),
|
|
249
|
+
"tx_present": tx is not None,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
251
252
|
|
|
252
253
|
# Default behavior: raising AlertError surfaces a UI alert.
|
|
253
254
|
# If `toast: true` is present in the payload, show a toast instead.
|
|
@@ -14,6 +14,7 @@ class AlertError(AppError):
|
|
|
14
14
|
title: str | None = None,
|
|
15
15
|
toast: bool = False,
|
|
16
16
|
variant: str | None = None,
|
|
17
|
+
log_to_cloudwatch: bool = False,
|
|
17
18
|
**payload,
|
|
18
19
|
):
|
|
19
20
|
# Backwards compatible:
|
|
@@ -24,13 +25,16 @@ class AlertError(AppError):
|
|
|
24
25
|
and title is None
|
|
25
26
|
and toast is False
|
|
26
27
|
and variant is None
|
|
28
|
+
and log_to_cloudwatch is False
|
|
27
29
|
and not payload
|
|
28
30
|
):
|
|
29
31
|
super().__init__(message)
|
|
32
|
+
self.log_to_cloudwatch = log_to_cloudwatch
|
|
30
33
|
return
|
|
31
34
|
|
|
32
|
-
if isinstance(message, dict) and title is None and toast is False and variant is None and not payload:
|
|
35
|
+
if isinstance(message, dict) and title is None and toast is False and variant is None and log_to_cloudwatch is False and not payload:
|
|
33
36
|
super().__init__(message)
|
|
37
|
+
self.log_to_cloudwatch = log_to_cloudwatch
|
|
34
38
|
return
|
|
35
39
|
|
|
36
40
|
data: dict = {}
|
|
@@ -49,6 +53,7 @@ class AlertError(AppError):
|
|
|
49
53
|
data.update(payload)
|
|
50
54
|
|
|
51
55
|
super().__init__(data)
|
|
56
|
+
self.log_to_cloudwatch = log_to_cloudwatch
|
|
52
57
|
|
|
53
58
|
def get_payload(self):
|
|
54
59
|
data = self.args[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>")
|