velocity-python 0.1.23__tar.gz → 0.1.25__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.23/src/velocity_python.egg-info → velocity_python-0.1.25}/PKG-INFO +1 -1
- {velocity_python-0.1.23 → velocity_python-0.1.25}/pyproject.toml +1 -1
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/__init__.py +39 -0
- velocity_python-0.1.25/src/velocity/aws/assets/__init__.py +57 -0
- velocity_python-0.1.25/src/velocity/aws/assets/backfill.py +140 -0
- velocity_python-0.1.25/src/velocity/aws/assets/indexing.py +217 -0
- velocity_python-0.1.25/src/velocity/aws/assets/references.py +263 -0
- velocity_python-0.1.25/src/velocity/aws/assets/service.py +275 -0
- velocity_python-0.1.25/src/velocity/aws/assets/usage_index.py +124 -0
- velocity_python-0.1.25/src/velocity/aws/dirty_pipeline.py +118 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/context.py +12 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/mixins/data_service.py +26 -0
- velocity_python-0.1.25/src/velocity/aws/ssm_config.py +94 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/postgres/__init__.py +14 -13
- {velocity_python-0.1.23 → velocity_python-0.1.25/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity_python.egg-info/SOURCES.txt +11 -0
- velocity_python-0.1.25/tests/test_asset_references.py +167 -0
- velocity_python-0.1.25/tests/test_assets_service.py +314 -0
- velocity_python-0.1.25/tests/test_dirty_pipeline_fast_path.py +118 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/LICENSE +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/README.md +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/setup.cfg +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_observability.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.23 → velocity_python-0.1.25}/tests/test_where_clause_validation.py +0 -0
|
@@ -38,6 +38,26 @@ _LAZY_ATTRS = {
|
|
|
38
38
|
"BACKOFFICE_BUCKET": "velocity.aws.s3",
|
|
39
39
|
"PRIVATE_BUCKETS": "velocity.aws.s3",
|
|
40
40
|
"ALLOWED_UPLOAD_BUCKETS": "velocity.aws.s3",
|
|
41
|
+
# Asset helpers
|
|
42
|
+
"AssetReference": "velocity.aws.assets",
|
|
43
|
+
"AssetUsage": "velocity.aws.assets",
|
|
44
|
+
"AssetUsageReconciliation": "velocity.aws.assets",
|
|
45
|
+
"AssetBackfillSummary": "velocity.aws.assets",
|
|
46
|
+
"AssetRecord": "velocity.aws.assets",
|
|
47
|
+
"AssetUploadResult": "velocity.aws.assets",
|
|
48
|
+
"backfill_asset_records": "velocity.aws.assets",
|
|
49
|
+
"build_asset_key": "velocity.aws.assets",
|
|
50
|
+
"build_asset_usages": "velocity.aws.assets",
|
|
51
|
+
"build_backfill_asset_data": "velocity.aws.assets",
|
|
52
|
+
"find_existing_asset": "velocity.aws.assets",
|
|
53
|
+
"extract_asset_references_from_html": "velocity.aws.assets",
|
|
54
|
+
"extract_asset_references_from_record": "velocity.aws.assets",
|
|
55
|
+
"extract_asset_references_from_value": "velocity.aws.assets",
|
|
56
|
+
"iter_s3_objects": "velocity.aws.assets",
|
|
57
|
+
"list_assets": "velocity.aws.assets",
|
|
58
|
+
"normalize_asset_record": "velocity.aws.assets",
|
|
59
|
+
"reconcile_asset_usages": "velocity.aws.assets",
|
|
60
|
+
"upload_image_asset": "velocity.aws.assets",
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
|
|
@@ -67,4 +87,23 @@ __all__ = [
|
|
|
67
87
|
"BACKOFFICE_BUCKET",
|
|
68
88
|
"PRIVATE_BUCKETS",
|
|
69
89
|
"ALLOWED_UPLOAD_BUCKETS",
|
|
90
|
+
"AssetBackfillSummary",
|
|
91
|
+
"AssetReference",
|
|
92
|
+
"AssetRecord",
|
|
93
|
+
"AssetUploadResult",
|
|
94
|
+
"AssetUsage",
|
|
95
|
+
"AssetUsageReconciliation",
|
|
96
|
+
"backfill_asset_records",
|
|
97
|
+
"build_asset_key",
|
|
98
|
+
"build_asset_usages",
|
|
99
|
+
"build_backfill_asset_data",
|
|
100
|
+
"extract_asset_references_from_html",
|
|
101
|
+
"extract_asset_references_from_record",
|
|
102
|
+
"extract_asset_references_from_value",
|
|
103
|
+
"find_existing_asset",
|
|
104
|
+
"iter_s3_objects",
|
|
105
|
+
"list_assets",
|
|
106
|
+
"normalize_asset_record",
|
|
107
|
+
"reconcile_asset_usages",
|
|
108
|
+
"upload_image_asset",
|
|
70
109
|
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from velocity.aws.assets.references import (
|
|
2
|
+
AssetReference,
|
|
3
|
+
extract_asset_references_from_html,
|
|
4
|
+
extract_asset_references_from_record,
|
|
5
|
+
extract_asset_references_from_value,
|
|
6
|
+
)
|
|
7
|
+
from velocity.aws.assets.backfill import (
|
|
8
|
+
AssetBackfillSummary,
|
|
9
|
+
backfill_asset_records,
|
|
10
|
+
build_backfill_asset_data,
|
|
11
|
+
iter_s3_objects,
|
|
12
|
+
)
|
|
13
|
+
from velocity.aws.assets.service import (
|
|
14
|
+
AssetRecord,
|
|
15
|
+
AssetUploadResult,
|
|
16
|
+
build_asset_key,
|
|
17
|
+
find_existing_asset,
|
|
18
|
+
list_assets,
|
|
19
|
+
normalize_asset_record,
|
|
20
|
+
upload_image_asset,
|
|
21
|
+
)
|
|
22
|
+
from velocity.aws.assets.usage_index import (
|
|
23
|
+
AssetUsage,
|
|
24
|
+
AssetUsageReconciliation,
|
|
25
|
+
build_asset_usages,
|
|
26
|
+
reconcile_asset_usages,
|
|
27
|
+
)
|
|
28
|
+
from velocity.aws.assets.indexing import (
|
|
29
|
+
FORM_BUILDER_TEMPLATE_HTML_FIELDS,
|
|
30
|
+
FORM_BUILDER_TEMPLATE_IMAGE_FIELDS,
|
|
31
|
+
reconcile_form_builder_template_asset_usages,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AssetReference",
|
|
36
|
+
"AssetBackfillSummary",
|
|
37
|
+
"AssetRecord",
|
|
38
|
+
"AssetUploadResult",
|
|
39
|
+
"AssetUsage",
|
|
40
|
+
"AssetUsageReconciliation",
|
|
41
|
+
"backfill_asset_records",
|
|
42
|
+
"build_asset_key",
|
|
43
|
+
"build_asset_usages",
|
|
44
|
+
"build_backfill_asset_data",
|
|
45
|
+
"extract_asset_references_from_html",
|
|
46
|
+
"extract_asset_references_from_record",
|
|
47
|
+
"extract_asset_references_from_value",
|
|
48
|
+
"find_existing_asset",
|
|
49
|
+
"FORM_BUILDER_TEMPLATE_HTML_FIELDS",
|
|
50
|
+
"FORM_BUILDER_TEMPLATE_IMAGE_FIELDS",
|
|
51
|
+
"iter_s3_objects",
|
|
52
|
+
"list_assets",
|
|
53
|
+
"normalize_asset_record",
|
|
54
|
+
"reconcile_form_builder_template_asset_usages",
|
|
55
|
+
"reconcile_asset_usages",
|
|
56
|
+
"upload_image_asset",
|
|
57
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
6
|
+
|
|
7
|
+
from velocity.aws.assets.service import AssetRecord, normalize_asset_record
|
|
8
|
+
from velocity.aws.s3 import PRIVATE_BUCKETS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
HeadLoader = Callable[[str, str], Mapping[str, Any] | None]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class AssetBackfillSummary:
|
|
16
|
+
scanned: int
|
|
17
|
+
inserted: int
|
|
18
|
+
skipped_existing: int
|
|
19
|
+
skipped_unsupported: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def iter_s3_objects(
|
|
23
|
+
bucket: str,
|
|
24
|
+
*,
|
|
25
|
+
prefix: str = "",
|
|
26
|
+
s3_client=None,
|
|
27
|
+
) -> Iterable[dict[str, Any]]:
|
|
28
|
+
if s3_client is None:
|
|
29
|
+
import boto3
|
|
30
|
+
|
|
31
|
+
s3_client = boto3.client("s3")
|
|
32
|
+
paginator = s3_client.get_paginator("list_objects_v2")
|
|
33
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
34
|
+
for row in page.get("Contents", []):
|
|
35
|
+
yield row
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_backfill_asset_data(
|
|
39
|
+
bucket: str,
|
|
40
|
+
object_info: Mapping[str, Any],
|
|
41
|
+
*,
|
|
42
|
+
head_info: Mapping[str, Any] | None = None,
|
|
43
|
+
) -> dict[str, Any] | None:
|
|
44
|
+
key = str(object_info.get("Key") or "").strip()
|
|
45
|
+
if not key or _is_thumbnail_key(key) or key.endswith("/"):
|
|
46
|
+
return None
|
|
47
|
+
metadata = dict((head_info or {}).get("Metadata") or {})
|
|
48
|
+
filename = os.path.basename(key)
|
|
49
|
+
file_ext = os.path.splitext(filename)[1][1:].lower() or None
|
|
50
|
+
row = {
|
|
51
|
+
"bucket": bucket,
|
|
52
|
+
"key": key,
|
|
53
|
+
"url": f"https://{bucket}.s3.amazonaws.com/{key}",
|
|
54
|
+
"public": bucket not in PRIVATE_BUCKETS,
|
|
55
|
+
"name": filename,
|
|
56
|
+
"type": file_ext,
|
|
57
|
+
"etag": _clean_etag(object_info.get("ETag") or (head_info or {}).get("ETag")),
|
|
58
|
+
"filesize": _coerce_int(object_info.get("Size") or (head_info or {}).get("ContentLength")),
|
|
59
|
+
"hash": metadata.get("hash"),
|
|
60
|
+
"thumbnail_key": metadata.get("thumbnail_key"),
|
|
61
|
+
"client": None if key.startswith("site/uploads") else key.split("/", 1)[0],
|
|
62
|
+
}
|
|
63
|
+
row.update(_copy_if_present(metadata, ["format", "width", "height", "is_animated"]))
|
|
64
|
+
return row
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def backfill_asset_records(
|
|
68
|
+
tx,
|
|
69
|
+
*,
|
|
70
|
+
bucket: str,
|
|
71
|
+
objects: Iterable[Mapping[str, Any]],
|
|
72
|
+
head_loader: HeadLoader | None = None,
|
|
73
|
+
) -> tuple[list[AssetRecord], AssetBackfillSummary]:
|
|
74
|
+
inserted_assets: list[AssetRecord] = []
|
|
75
|
+
scanned = 0
|
|
76
|
+
inserted = 0
|
|
77
|
+
skipped_existing = 0
|
|
78
|
+
skipped_unsupported = 0
|
|
79
|
+
|
|
80
|
+
for object_info in objects:
|
|
81
|
+
scanned += 1
|
|
82
|
+
key = str(object_info.get("Key") or "").strip()
|
|
83
|
+
if not key or _is_thumbnail_key(key) or key.endswith("/"):
|
|
84
|
+
skipped_unsupported += 1
|
|
85
|
+
continue
|
|
86
|
+
if tx.table("images").find({"key": key}):
|
|
87
|
+
skipped_existing += 1
|
|
88
|
+
continue
|
|
89
|
+
head_info = head_loader(bucket, key) if head_loader else None
|
|
90
|
+
row = build_backfill_asset_data(bucket, object_info, head_info=head_info)
|
|
91
|
+
if not row:
|
|
92
|
+
skipped_unsupported += 1
|
|
93
|
+
continue
|
|
94
|
+
tx.table("images").upsert(row, {"key": row["key"]})
|
|
95
|
+
inserted += 1
|
|
96
|
+
stored = tx.table("images").find({"key": row["key"]})
|
|
97
|
+
inserted_assets.append(normalize_asset_record(stored.to_dict() if stored else row))
|
|
98
|
+
|
|
99
|
+
return inserted_assets, AssetBackfillSummary(
|
|
100
|
+
scanned=scanned,
|
|
101
|
+
inserted=inserted,
|
|
102
|
+
skipped_existing=skipped_existing,
|
|
103
|
+
skipped_unsupported=skipped_unsupported,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_thumbnail_key(key: str) -> bool:
|
|
108
|
+
stem, ext = os.path.splitext(key)
|
|
109
|
+
return stem.endswith("-thumb") and ext.lower() == ".png"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _clean_etag(value: Any) -> str | None:
|
|
113
|
+
if value is None:
|
|
114
|
+
return None
|
|
115
|
+
text = str(value).strip().strip('"')
|
|
116
|
+
return text or None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _coerce_int(value: Any) -> int | None:
|
|
120
|
+
if value in (None, ""):
|
|
121
|
+
return None
|
|
122
|
+
try:
|
|
123
|
+
return int(value)
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _copy_if_present(source: Mapping[str, Any], keys: list[str]) -> dict[str, Any]:
|
|
129
|
+
result: dict[str, Any] = {}
|
|
130
|
+
for key in keys:
|
|
131
|
+
if key not in source:
|
|
132
|
+
continue
|
|
133
|
+
value = source[key]
|
|
134
|
+
if key in {"width", "height"}:
|
|
135
|
+
result[key] = _coerce_int(value)
|
|
136
|
+
elif key == "is_animated":
|
|
137
|
+
result[key] = str(value).lower() in {"1", "true", "yes", "y"}
|
|
138
|
+
else:
|
|
139
|
+
result[key] = value
|
|
140
|
+
return result
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
from velocity.aws.assets.references import AssetReference, extract_asset_references_from_record
|
|
6
|
+
from velocity.aws.assets.usage_index import (
|
|
7
|
+
AssetUsage,
|
|
8
|
+
AssetUsageReconciliation,
|
|
9
|
+
build_asset_usages,
|
|
10
|
+
reconcile_asset_usages,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FORM_BUILDER_TEMPLATE_HTML_FIELDS = (
|
|
15
|
+
"body_message_text",
|
|
16
|
+
"form_instructions",
|
|
17
|
+
"confirmation_body_text",
|
|
18
|
+
"receipt_body_text",
|
|
19
|
+
"rally_receipt_body_text",
|
|
20
|
+
"pii_instructions",
|
|
21
|
+
"metric_section_instructions",
|
|
22
|
+
"donation_section_instructions",
|
|
23
|
+
*tuple(f"donation_{index}_html" for index in range(1, 10)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
FORM_BUILDER_TEMPLATE_IMAGE_FIELDS = (
|
|
27
|
+
"receipt_header_image",
|
|
28
|
+
"secure_image_url",
|
|
29
|
+
"header_banner_image_url",
|
|
30
|
+
"background_image_url",
|
|
31
|
+
"main_image_url",
|
|
32
|
+
"body_header_url",
|
|
33
|
+
"spot_image",
|
|
34
|
+
"confirmation_header_image",
|
|
35
|
+
"confirmation_body_image",
|
|
36
|
+
*tuple(f"donation_{index}_image" for index in range(1, 10)),
|
|
37
|
+
*tuple(f"metric_{index}_image" for index in range(1, 10)),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reconcile_form_builder_template_asset_usages(
|
|
42
|
+
tx,
|
|
43
|
+
row: Mapping[str, Any] | Any,
|
|
44
|
+
*,
|
|
45
|
+
html_fields=FORM_BUILDER_TEMPLATE_HTML_FIELDS,
|
|
46
|
+
image_fields=FORM_BUILDER_TEMPLATE_IMAGE_FIELDS,
|
|
47
|
+
last_indexed_by: str = "velocity.aws.dirty_pipeline",
|
|
48
|
+
) -> AssetUsageReconciliation:
|
|
49
|
+
source_row = _row_to_dict(row)
|
|
50
|
+
source_sys_id = _as_string(source_row.get("sys_id"))
|
|
51
|
+
if not source_sys_id:
|
|
52
|
+
raise ValueError("form_builder_template row must include sys_id")
|
|
53
|
+
|
|
54
|
+
references = extract_asset_references_from_record(
|
|
55
|
+
source_row,
|
|
56
|
+
html_fields=html_fields,
|
|
57
|
+
image_fields=image_fields,
|
|
58
|
+
)
|
|
59
|
+
desired = build_asset_usages(
|
|
60
|
+
references,
|
|
61
|
+
source_table="form_builder_template",
|
|
62
|
+
source_sys_id=source_sys_id,
|
|
63
|
+
source_record_label=_build_source_record_label(source_row),
|
|
64
|
+
last_source_modified=_as_string(source_row.get("sys_modified")),
|
|
65
|
+
last_indexed_by=last_indexed_by,
|
|
66
|
+
asset_id_resolver=lambda reference: resolve_asset_id_for_reference(tx, reference),
|
|
67
|
+
)
|
|
68
|
+
existing = load_asset_usages(
|
|
69
|
+
tx,
|
|
70
|
+
source_table="form_builder_template",
|
|
71
|
+
source_sys_id=source_sys_id,
|
|
72
|
+
)
|
|
73
|
+
reconciliation = reconcile_asset_usages(existing, desired)
|
|
74
|
+
apply_asset_usage_reconciliation(tx, reconciliation)
|
|
75
|
+
return reconciliation
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_asset_usages(tx, *, source_table: str, source_sys_id: str) -> list[AssetUsage]:
|
|
79
|
+
rows = tx.table("asset_usage_index").select(
|
|
80
|
+
where={
|
|
81
|
+
"source_table": source_table,
|
|
82
|
+
"source_sys_id": source_sys_id,
|
|
83
|
+
}
|
|
84
|
+
).all()
|
|
85
|
+
return [_row_to_asset_usage(row) for row in rows]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def apply_asset_usage_reconciliation(tx, reconciliation: AssetUsageReconciliation) -> None:
|
|
89
|
+
table = tx.table("asset_usage_index")
|
|
90
|
+
for usage in reconciliation.to_create + reconciliation.to_refresh:
|
|
91
|
+
row = _asset_usage_to_row(usage)
|
|
92
|
+
table.upsert(
|
|
93
|
+
row,
|
|
94
|
+
{
|
|
95
|
+
"asset_id": row["asset_id"],
|
|
96
|
+
"source_table": row["source_table"],
|
|
97
|
+
"source_sys_id": row["source_sys_id"],
|
|
98
|
+
"source_field": row["source_field"],
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for usage in reconciliation.to_delete:
|
|
103
|
+
table.delete(
|
|
104
|
+
where={
|
|
105
|
+
"asset_id": usage.asset_id,
|
|
106
|
+
"source_table": usage.source_table,
|
|
107
|
+
"source_sys_id": usage.source_sys_id,
|
|
108
|
+
"source_field": usage.source_field,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_asset_id_for_reference(tx, reference: AssetReference) -> str | None:
|
|
114
|
+
if reference.asset_id:
|
|
115
|
+
return reference.asset_id
|
|
116
|
+
|
|
117
|
+
images = tx.table("images")
|
|
118
|
+
for candidate_key in _candidate_storage_keys(reference.storage_key):
|
|
119
|
+
row = images.find({"key": candidate_key})
|
|
120
|
+
asset_id = _extract_sys_id(row)
|
|
121
|
+
if asset_id:
|
|
122
|
+
return asset_id
|
|
123
|
+
|
|
124
|
+
if reference.url:
|
|
125
|
+
row = images.find({"url": reference.url})
|
|
126
|
+
asset_id = _extract_sys_id(row)
|
|
127
|
+
if asset_id:
|
|
128
|
+
return asset_id
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _candidate_storage_keys(storage_key: str | None) -> list[str]:
|
|
134
|
+
if not storage_key:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
raw_value = storage_key.strip()
|
|
138
|
+
normalized_value = raw_value.lstrip("/")
|
|
139
|
+
candidates = [
|
|
140
|
+
raw_value,
|
|
141
|
+
normalized_value,
|
|
142
|
+
f"/{normalized_value}",
|
|
143
|
+
f"//{normalized_value}",
|
|
144
|
+
]
|
|
145
|
+
deduped: list[str] = []
|
|
146
|
+
for candidate in candidates:
|
|
147
|
+
if candidate and candidate not in deduped:
|
|
148
|
+
deduped.append(candidate)
|
|
149
|
+
return deduped
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _row_to_asset_usage(row: Mapping[str, Any] | Any) -> AssetUsage:
|
|
153
|
+
data = _row_to_dict(row)
|
|
154
|
+
return AssetUsage(
|
|
155
|
+
asset_id=_as_string(data.get("asset_id")) or "",
|
|
156
|
+
source_table=_as_string(data.get("source_table")) or "",
|
|
157
|
+
source_sys_id=_as_string(data.get("source_sys_id")) or "",
|
|
158
|
+
source_field=_as_string(data.get("source_field")) or "",
|
|
159
|
+
source_field_type=_as_string(data.get("source_field_type")) or "",
|
|
160
|
+
asset_reference_value=_as_string(data.get("asset_reference_value")),
|
|
161
|
+
asset_storage_key_snapshot=_as_string(data.get("asset_storage_key_snapshot")),
|
|
162
|
+
asset_public_url_snapshot=_as_string(data.get("asset_public_url_snapshot")),
|
|
163
|
+
source_record_label=_as_string(data.get("source_record_label")),
|
|
164
|
+
last_source_modified=_as_string(data.get("last_source_modified")),
|
|
165
|
+
last_indexed_by=_as_string(data.get("last_indexed_by")),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _asset_usage_to_row(usage: AssetUsage) -> dict[str, Any]:
|
|
170
|
+
return {
|
|
171
|
+
"asset_id": usage.asset_id,
|
|
172
|
+
"source_table": usage.source_table,
|
|
173
|
+
"source_sys_id": usage.source_sys_id,
|
|
174
|
+
"source_field": usage.source_field,
|
|
175
|
+
"source_field_type": usage.source_field_type,
|
|
176
|
+
"asset_reference_value": usage.asset_reference_value,
|
|
177
|
+
"asset_storage_key_snapshot": usage.asset_storage_key_snapshot,
|
|
178
|
+
"asset_public_url_snapshot": usage.asset_public_url_snapshot,
|
|
179
|
+
"source_record_label": usage.source_record_label,
|
|
180
|
+
"last_source_modified": usage.last_source_modified,
|
|
181
|
+
"last_indexed_by": usage.last_indexed_by,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _build_source_record_label(row: Mapping[str, Any]) -> str | None:
|
|
186
|
+
for key in ("description", "page_title", "campaign"):
|
|
187
|
+
label = _as_string(row.get(key))
|
|
188
|
+
if label:
|
|
189
|
+
return label
|
|
190
|
+
|
|
191
|
+
client = _as_string(row.get("client"))
|
|
192
|
+
campaign = _as_string(row.get("campaign"))
|
|
193
|
+
if client and campaign:
|
|
194
|
+
return f"{client} - {campaign}"
|
|
195
|
+
return client or campaign
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _extract_sys_id(row: Mapping[str, Any] | Any) -> str | None:
|
|
199
|
+
data = _row_to_dict(row)
|
|
200
|
+
return _as_string(data.get("sys_id"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _row_to_dict(row: Mapping[str, Any] | Any) -> dict[str, Any]:
|
|
204
|
+
if row is None:
|
|
205
|
+
return {}
|
|
206
|
+
if hasattr(row, "to_dict"):
|
|
207
|
+
return row.to_dict()
|
|
208
|
+
if isinstance(row, Mapping):
|
|
209
|
+
return dict(row)
|
|
210
|
+
raise TypeError(f"Unsupported row type: {type(row)!r}")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _as_string(value: Any) -> str | None:
|
|
214
|
+
if value is None:
|
|
215
|
+
return None
|
|
216
|
+
text = str(value).strip()
|
|
217
|
+
return text or None
|