velocity-python 0.1.74__tar.gz → 0.1.76__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.74 → velocity_python-0.1.76}/PKG-INFO +1 -1
- {velocity_python-0.1.74 → velocity_python-0.1.76}/pyproject.toml +1 -1
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/context.py +105 -6
- velocity_python-0.1.76/src/velocity/db/maintenance.py +319 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/SOURCES.txt +3 -0
- velocity_python-0.1.76/tests/test_db_maintenance.py +181 -0
- velocity_python-0.1.76/tests/test_enqueue_send_failures.py +146 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/LICENSE +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/README.md +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/setup.cfg +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_observability.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.74 → velocity_python-0.1.76}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -36,6 +36,38 @@ def _get_work_queue_name() -> str:
|
|
|
36
36
|
return queue_name
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _emit_enqueue_failure_metric(action, count):
|
|
40
|
+
"""Emit a CloudWatch ``CaringCent/Enqueue SendFailures`` metric.
|
|
41
|
+
|
|
42
|
+
SQS send failures are a pre-queue loss the dead-letter queue can never see
|
|
43
|
+
(a message that was never sent never reaches the DLQ), so this metric makes
|
|
44
|
+
send-side losses observable/alarmable the same way DLQ arrivals are.
|
|
45
|
+
Best-effort: never raises, so a metrics outage cannot break enqueueing.
|
|
46
|
+
"""
|
|
47
|
+
if not count:
|
|
48
|
+
return
|
|
49
|
+
try:
|
|
50
|
+
stage = config_get_stage("") or "unknown"
|
|
51
|
+
boto3.client("cloudwatch").put_metric_data(
|
|
52
|
+
Namespace="CaringCent/Enqueue",
|
|
53
|
+
MetricData=[
|
|
54
|
+
{
|
|
55
|
+
"MetricName": "SendFailures",
|
|
56
|
+
"Value": float(count),
|
|
57
|
+
"Unit": "Count",
|
|
58
|
+
"Dimensions": [{"Name": "Stage", "Value": str(stage)}],
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Failed to emit enqueue SendFailures metric (action=%s, count=%s)",
|
|
65
|
+
action,
|
|
66
|
+
count,
|
|
67
|
+
exc_info=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
39
71
|
def _get_controls_vars() -> dict:
|
|
40
72
|
app_stage = config_get_stage()
|
|
41
73
|
return {
|
|
@@ -266,6 +298,32 @@ class Context:
|
|
|
266
298
|
self._job_record_cache.pop(job_id, None)
|
|
267
299
|
tx.commit()
|
|
268
300
|
|
|
301
|
+
def fail_enqueue(self, tx, job_id=None, reason=None):
|
|
302
|
+
"""Mark a job-activity row Failed when its SQS message could not be sent.
|
|
303
|
+
|
|
304
|
+
``enqueue`` writes the ``aws_job_activity`` row (status ``Initialized``)
|
|
305
|
+
*before* sending to SQS. If the send fails and is never reconciled, the
|
|
306
|
+
row is stuck in ``Initialized`` forever — never consumed, and never
|
|
307
|
+
counted as a failure (the consumer's ``onError`` is the only thing that
|
|
308
|
+
writes ``Failed``). This flips such an orphaned row to ``Failed`` so it
|
|
309
|
+
surfaces on the operational dashboard instead of inflating the stuck
|
|
310
|
+
count. Runs in its own transaction like ``create_job``/``update_job``.
|
|
311
|
+
"""
|
|
312
|
+
if not job_id:
|
|
313
|
+
return
|
|
314
|
+
tx.table(self.JOB_ACTIVITY_TABLE).update(
|
|
315
|
+
self._sanitize_job_data(
|
|
316
|
+
{
|
|
317
|
+
"status": "Failed",
|
|
318
|
+
"message": "Job enqueue failed (SQS send failure)",
|
|
319
|
+
"error": reason or "SQS send failed",
|
|
320
|
+
}
|
|
321
|
+
),
|
|
322
|
+
{"job_id": job_id},
|
|
323
|
+
)
|
|
324
|
+
self._job_record_cache.pop(job_id, None)
|
|
325
|
+
tx.commit()
|
|
326
|
+
|
|
269
327
|
def _sanitize_job_data(self, data):
|
|
270
328
|
"""Sanitize sensitive data before storing in aws_job_activity table."""
|
|
271
329
|
if not isinstance(data, dict):
|
|
@@ -444,6 +502,50 @@ class Context:
|
|
|
444
502
|
messages = []
|
|
445
503
|
if user is None:
|
|
446
504
|
user = self.session().get("email_address") or "EnqueueTasks"
|
|
505
|
+
|
|
506
|
+
def _flush(batch):
|
|
507
|
+
"""Send one batch and reconcile SQS send failures.
|
|
508
|
+
|
|
509
|
+
A failed send would otherwise leave the job-activity row stuck in
|
|
510
|
+
``Initialized`` forever, so transient (non-``SenderFault``) failures
|
|
511
|
+
are retried once and any message that still never reaches the queue
|
|
512
|
+
has its job row marked ``Failed`` via ``fail_enqueue``.
|
|
513
|
+
"""
|
|
514
|
+
nonlocal results
|
|
515
|
+
if not batch:
|
|
516
|
+
return
|
|
517
|
+
result = queue.send_messages(Entries=batch) or {}
|
|
518
|
+
results = deep_merge(results, result)
|
|
519
|
+
failed = result.get("Failed") or []
|
|
520
|
+
if not failed:
|
|
521
|
+
return
|
|
522
|
+
by_id = {entry["Id"]: entry for entry in batch}
|
|
523
|
+
still_failed = {f["Id"]: f for f in failed if f.get("SenderFault")}
|
|
524
|
+
transient = [
|
|
525
|
+
by_id[f["Id"]]
|
|
526
|
+
for f in failed
|
|
527
|
+
if not f.get("SenderFault") and f.get("Id") in by_id
|
|
528
|
+
]
|
|
529
|
+
if transient:
|
|
530
|
+
# The retry result is consulted only for entries that still
|
|
531
|
+
# failed; it is not merged into ``results`` because ``deep_merge``
|
|
532
|
+
# cannot dedup SQS's list-of-dict ``Successful``/``Failed`` payloads.
|
|
533
|
+
retry = queue.send_messages(Entries=transient) or {}
|
|
534
|
+
for f in retry.get("Failed") or []:
|
|
535
|
+
still_failed[f["Id"]] = f
|
|
536
|
+
if not still_failed:
|
|
537
|
+
return
|
|
538
|
+
if not suppress_job_activity:
|
|
539
|
+
for job_id, f in still_failed.items():
|
|
540
|
+
self.fail_enqueue(
|
|
541
|
+
job_id=job_id,
|
|
542
|
+
reason="SQS send failed: {} {}".format(
|
|
543
|
+
f.get("Code") or "", f.get("Message") or ""
|
|
544
|
+
).strip(),
|
|
545
|
+
)
|
|
546
|
+
_emit_enqueue_failure_metric(action, len(still_failed))
|
|
547
|
+
results.setdefault("FailedToEnqueue", []).extend(still_failed.keys())
|
|
548
|
+
|
|
447
549
|
for item in payload:
|
|
448
550
|
message = {"action": action, "payload": item}
|
|
449
551
|
id = str(uuid.uuid4()).split("-")[0]
|
|
@@ -474,13 +576,10 @@ class Context:
|
|
|
474
576
|
messages.append({"Id": id, "MessageBody": to_json(message)})
|
|
475
577
|
|
|
476
578
|
if len(messages) == 10:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
messages.clear()
|
|
579
|
+
_flush(messages)
|
|
580
|
+
messages = []
|
|
480
581
|
|
|
481
|
-
|
|
482
|
-
result = queue.send_messages(Entries=messages)
|
|
483
|
-
results = deep_merge(results, result)
|
|
582
|
+
_flush(messages)
|
|
484
583
|
|
|
485
584
|
return results
|
|
486
585
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""PostgreSQL database maintenance toolkit.
|
|
2
|
+
|
|
3
|
+
Safe, efficient database-admin operations that velocity's normal transactional
|
|
4
|
+
model can't express. ``CREATE``/``DROP``/``RENAME DATABASE`` and template clones
|
|
5
|
+
must run in **autocommit** and must **not** run while connected to the source or
|
|
6
|
+
destination database. ``DatabaseMaintenance`` connects to a neutral *maintenance*
|
|
7
|
+
database (default ``postgres``) so the source/dest are never the active session,
|
|
8
|
+
runs DDL in autocommit, and terminates other sessions as needed.
|
|
9
|
+
|
|
10
|
+
from velocity.db import maintenance
|
|
11
|
+
|
|
12
|
+
m = maintenance.DatabaseMaintenance() # env (DBHost/DBUser/...), maint db 'postgres'
|
|
13
|
+
|
|
14
|
+
# Server-side template clone -- instant, no dump/restore round-trip:
|
|
15
|
+
m.refresh("caringcent-production", "caringcent-develop") # snapshot dest, then clone source -> dest
|
|
16
|
+
m.snapshot("caringcent-develop") # -> caringcent-develop-snapshot-<ts>
|
|
17
|
+
m.prune_snapshots("caringcent-develop", keep=3) # drop all but the 3 newest snapshots
|
|
18
|
+
|
|
19
|
+
Every database name is identifier-quoted; string-literal comparisons are escaped.
|
|
20
|
+
A configurable ``protected`` set blocks destructive ops (drop / rename / overwrite)
|
|
21
|
+
on critical databases (default: ``caringcent-production``).
|
|
22
|
+
|
|
23
|
+
Snapshot databases are named ``<db>-snapshot-<YYYYMMDD-HHMMSS>`` -- a **sortable**
|
|
24
|
+
timestamp, so listing/pruning by name is chronological.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import datetime
|
|
30
|
+
import os
|
|
31
|
+
from typing import Iterable, List, Optional
|
|
32
|
+
|
|
33
|
+
from velocity.db.servers import postgres
|
|
34
|
+
|
|
35
|
+
SNAPSHOT_INFIX = "-snapshot-"
|
|
36
|
+
TS_FORMAT = "%Y%m%d-%H%M%S" # sortable: lexical order == chronological order
|
|
37
|
+
DEFAULT_PROTECTED = frozenset({"caringcent-production"})
|
|
38
|
+
|
|
39
|
+
_BACKUP_FORMAT_FLAGS = {"custom": "-Fc", "plain": "-Fp", "directory": "-Fd", "tar": "-Ft"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def quote_identifier(name: str) -> str:
|
|
43
|
+
"""Quote a database identifier (doubles embedded double-quotes)."""
|
|
44
|
+
return '"' + str(name).replace('"', '""') + '"'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def quote_literal(value: str) -> str:
|
|
48
|
+
"""Quote a string literal (doubles embedded single-quotes)."""
|
|
49
|
+
return "'" + str(value).replace("'", "''") + "'"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def timestamp() -> str:
|
|
53
|
+
return datetime.datetime.now().strftime(TS_FORMAT)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DatabaseMaintenance:
|
|
57
|
+
"""Connection-pooled, autocommit-aware PostgreSQL admin operations."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
maintenance_db: str = "postgres",
|
|
63
|
+
config: Optional[dict] = None,
|
|
64
|
+
protected: Iterable[str] = DEFAULT_PROTECTED,
|
|
65
|
+
engine=None,
|
|
66
|
+
):
|
|
67
|
+
self.maintenance_db = maintenance_db
|
|
68
|
+
self.protected = frozenset(protected or ())
|
|
69
|
+
self._config = dict(config or {})
|
|
70
|
+
# Lazily initialized so construction never opens a connection (keeps the
|
|
71
|
+
# class unit-testable without a live database).
|
|
72
|
+
self._engine = engine
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------ engine
|
|
75
|
+
def _get_engine(self):
|
|
76
|
+
if self._engine is None:
|
|
77
|
+
cfg = {"dbname": self.maintenance_db}
|
|
78
|
+
cfg.update(self._config)
|
|
79
|
+
self._engine = postgres.initialize(config=cfg)
|
|
80
|
+
return self._engine
|
|
81
|
+
|
|
82
|
+
def _exec(self, *statements: str) -> None:
|
|
83
|
+
"""Run DDL/admin statements in autocommit (required for CREATE/DROP/RENAME DATABASE)."""
|
|
84
|
+
engine = self._get_engine()
|
|
85
|
+
|
|
86
|
+
@engine.transaction
|
|
87
|
+
def _run(tx):
|
|
88
|
+
tx.rollback() # discard the implicit transaction opened on checkout
|
|
89
|
+
tx.connection.autocommit = True
|
|
90
|
+
for sql in statements:
|
|
91
|
+
if sql:
|
|
92
|
+
tx.execute(sql)
|
|
93
|
+
|
|
94
|
+
_run()
|
|
95
|
+
|
|
96
|
+
def _query(self, sql: str) -> List[dict]:
|
|
97
|
+
engine = self._get_engine()
|
|
98
|
+
|
|
99
|
+
@engine.transaction
|
|
100
|
+
def _run(tx):
|
|
101
|
+
return list(tx.execute(sql).as_dict())
|
|
102
|
+
|
|
103
|
+
return _run()
|
|
104
|
+
|
|
105
|
+
def _conn_params(self) -> dict:
|
|
106
|
+
return {
|
|
107
|
+
"host": self._config.get("host") or os.environ.get("DBHost"),
|
|
108
|
+
"port": self._config.get("port") or os.environ.get("DBPort"),
|
|
109
|
+
"user": self._config.get("user") or os.environ.get("DBUser"),
|
|
110
|
+
"password": self._config.get("password") or os.environ.get("DBPassword"),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------ guards
|
|
114
|
+
def _guard(self, db: str, action: str) -> None:
|
|
115
|
+
if db in self.protected:
|
|
116
|
+
raise PermissionError(
|
|
117
|
+
f"Refusing to {action} protected database {db!r}. "
|
|
118
|
+
"Construct with protected=... to override."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ SQL helpers
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _terminate_sql(db: str) -> str:
|
|
124
|
+
return (
|
|
125
|
+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
|
|
126
|
+
f"WHERE datname = {quote_literal(db)} AND pid <> pg_backend_pid()"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------ queries
|
|
130
|
+
def exists(self, db: str) -> bool:
|
|
131
|
+
return bool(
|
|
132
|
+
self._query(f"SELECT 1 AS x FROM pg_database WHERE datname = {quote_literal(db)}")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def list_databases(self, *, pattern: Optional[str] = None) -> List[str]:
|
|
136
|
+
where = "WHERE datistemplate = false"
|
|
137
|
+
if pattern:
|
|
138
|
+
where += f" AND datname LIKE {quote_literal(pattern)}"
|
|
139
|
+
rows = self._query(f"SELECT datname FROM pg_database {where} ORDER BY datname")
|
|
140
|
+
return [r["datname"] for r in rows]
|
|
141
|
+
|
|
142
|
+
def list_snapshots(self, db: str) -> List[str]:
|
|
143
|
+
"""Snapshot databases for ``db``, oldest first (sortable timestamp suffix)."""
|
|
144
|
+
return sorted(self.list_databases(pattern=f"{db}{SNAPSHOT_INFIX}%"))
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------ connection control
|
|
147
|
+
def terminate_connections(self, db: str) -> None:
|
|
148
|
+
self._exec(self._terminate_sql(db))
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------ core ops
|
|
151
|
+
def clone(self, source: str, dest: str, *, drop_existing: bool = False) -> str:
|
|
152
|
+
"""``CREATE DATABASE dest WITH TEMPLATE source`` (server-side, near-instant)."""
|
|
153
|
+
self._guard(dest, "overwrite")
|
|
154
|
+
if self.exists(dest):
|
|
155
|
+
if not drop_existing:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Destination {dest!r} already exists. Pass drop_existing=True to replace it."
|
|
158
|
+
)
|
|
159
|
+
self.drop(dest)
|
|
160
|
+
# A template clone requires no other sessions on the source.
|
|
161
|
+
self._exec(
|
|
162
|
+
self._terminate_sql(source),
|
|
163
|
+
f"CREATE DATABASE {quote_identifier(dest)} WITH TEMPLATE {quote_identifier(source)}",
|
|
164
|
+
)
|
|
165
|
+
return dest
|
|
166
|
+
|
|
167
|
+
def snapshot(self, db: str, *, label: Optional[str] = None) -> str:
|
|
168
|
+
"""Clone ``db`` to ``<db>-snapshot-<ts>`` (or a custom ``label``)."""
|
|
169
|
+
dest = f"{db}{SNAPSHOT_INFIX}{label or timestamp()}"
|
|
170
|
+
return self.clone(db, dest)
|
|
171
|
+
|
|
172
|
+
def rename(self, old: str, new: str) -> str:
|
|
173
|
+
self._guard(old, "rename")
|
|
174
|
+
self._exec(
|
|
175
|
+
self._terminate_sql(old),
|
|
176
|
+
f"ALTER DATABASE {quote_identifier(old)} RENAME TO {quote_identifier(new)}",
|
|
177
|
+
)
|
|
178
|
+
return new
|
|
179
|
+
|
|
180
|
+
def drop(self, db: str, *, if_exists: bool = True) -> None:
|
|
181
|
+
self._guard(db, "drop")
|
|
182
|
+
clause = "DROP DATABASE IF EXISTS" if if_exists else "DROP DATABASE"
|
|
183
|
+
self._exec(self._terminate_sql(db), f"{clause} {quote_identifier(db)}")
|
|
184
|
+
|
|
185
|
+
def refresh(self, source: str, dest: str, *, keep_snapshot: bool = True) -> str:
|
|
186
|
+
"""Replace ``dest`` with a fresh template-clone of ``source``.
|
|
187
|
+
|
|
188
|
+
Safe sequence: terminate ``dest`` sessions -> rename ``dest`` to
|
|
189
|
+
``dest-snapshot-<ts>`` (or drop it when ``keep_snapshot=False``) -> clone
|
|
190
|
+
``source`` -> ``dest``. Keeping the snapshot makes the refresh reversible.
|
|
191
|
+
"""
|
|
192
|
+
self._guard(dest, "overwrite")
|
|
193
|
+
if self.exists(dest):
|
|
194
|
+
if keep_snapshot:
|
|
195
|
+
self.rename(dest, f"{dest}{SNAPSHOT_INFIX}{timestamp()}")
|
|
196
|
+
else:
|
|
197
|
+
self.drop(dest)
|
|
198
|
+
return self.clone(source, dest)
|
|
199
|
+
|
|
200
|
+
def prune_snapshots(
|
|
201
|
+
self,
|
|
202
|
+
db: str,
|
|
203
|
+
*,
|
|
204
|
+
keep: int = 3,
|
|
205
|
+
older_than_days: Optional[int] = None,
|
|
206
|
+
dry_run: bool = False,
|
|
207
|
+
) -> List[str]:
|
|
208
|
+
"""Drop old snapshot databases for ``db``.
|
|
209
|
+
|
|
210
|
+
With ``older_than_days`` set, drops snapshots whose timestamp is older than
|
|
211
|
+
the cutoff. Otherwise keeps the ``keep`` newest and drops the rest. Returns
|
|
212
|
+
the names dropped (or that would be dropped, when ``dry_run=True``).
|
|
213
|
+
"""
|
|
214
|
+
snaps = self.list_snapshots(db) # ascending (oldest first)
|
|
215
|
+
to_drop: List[str] = []
|
|
216
|
+
if older_than_days is not None:
|
|
217
|
+
cutoff = datetime.datetime.now() - datetime.timedelta(days=older_than_days)
|
|
218
|
+
prefix = f"{db}{SNAPSHOT_INFIX}"
|
|
219
|
+
for name in snaps:
|
|
220
|
+
suffix = name[len(prefix):]
|
|
221
|
+
try:
|
|
222
|
+
when = datetime.datetime.strptime(suffix, TS_FORMAT)
|
|
223
|
+
except ValueError:
|
|
224
|
+
continue # unrecognized suffix -> leave it alone
|
|
225
|
+
if when < cutoff:
|
|
226
|
+
to_drop.append(name)
|
|
227
|
+
elif keep >= 0 and len(snaps) > keep:
|
|
228
|
+
to_drop = snaps[: len(snaps) - keep]
|
|
229
|
+
|
|
230
|
+
if not dry_run:
|
|
231
|
+
for name in to_drop:
|
|
232
|
+
self.drop(name)
|
|
233
|
+
return to_drop
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------ portable backup/restore
|
|
236
|
+
def backup_to_file(
|
|
237
|
+
self, db: str, path: str, *, fmt: str = "custom", extra_args: Iterable[str] = ()
|
|
238
|
+
) -> str:
|
|
239
|
+
"""Portable off-instance backup via ``pg_dump`` (use for cross-server moves;
|
|
240
|
+
prefer ``snapshot``/``clone`` for same-server copies). ``extra_args`` are
|
|
241
|
+
passed through to ``pg_dump`` (e.g. ``("-O",)`` for no-owner)."""
|
|
242
|
+
if fmt not in _BACKUP_FORMAT_FLAGS:
|
|
243
|
+
raise ValueError(f"Unknown fmt {fmt!r}; choose one of {sorted(_BACKUP_FORMAT_FLAGS)}.")
|
|
244
|
+
p = self._conn_params()
|
|
245
|
+
self._run_tool(
|
|
246
|
+
["pg_dump", "-h", str(p["host"]), "-p", str(p["port"]), "-U", str(p["user"]),
|
|
247
|
+
_BACKUP_FORMAT_FLAGS[fmt], *extra_args, "-f", path, db]
|
|
248
|
+
)
|
|
249
|
+
return path
|
|
250
|
+
|
|
251
|
+
def restore_from_file(
|
|
252
|
+
self,
|
|
253
|
+
path: str,
|
|
254
|
+
dest: str,
|
|
255
|
+
*,
|
|
256
|
+
fmt: str = "custom",
|
|
257
|
+
drop_existing: bool = False,
|
|
258
|
+
extra_args: Iterable[str] = (),
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Restore a ``pg_dump`` file into a freshly created ``dest`` database.
|
|
261
|
+
``extra_args`` are passed to ``pg_restore``/``psql``."""
|
|
262
|
+
self._guard(dest, "overwrite")
|
|
263
|
+
if self.exists(dest):
|
|
264
|
+
if not drop_existing:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"Destination {dest!r} already exists. Pass drop_existing=True to replace it."
|
|
267
|
+
)
|
|
268
|
+
self.drop(dest)
|
|
269
|
+
self._exec(f"CREATE DATABASE {quote_identifier(dest)}")
|
|
270
|
+
p = self._conn_params()
|
|
271
|
+
base = ["-h", str(p["host"]), "-p", str(p["port"]), "-U", str(p["user"])]
|
|
272
|
+
if fmt == "plain":
|
|
273
|
+
self._run_tool(["psql", *base, *extra_args, "-d", dest, "-f", path])
|
|
274
|
+
else:
|
|
275
|
+
self._run_tool(["pg_restore", *base, *extra_args, "-d", dest, path])
|
|
276
|
+
return dest
|
|
277
|
+
|
|
278
|
+
def _run_tool(self, cmd: List[str]) -> None:
|
|
279
|
+
import subprocess
|
|
280
|
+
|
|
281
|
+
env = dict(os.environ)
|
|
282
|
+
password = self._conn_params().get("password")
|
|
283
|
+
if password:
|
|
284
|
+
env["PGPASSWORD"] = str(password)
|
|
285
|
+
subprocess.run(cmd, check=True, env=env, capture_output=True, text=True)
|
|
286
|
+
|
|
287
|
+
# ------------------------------------------------------------------ maintenance
|
|
288
|
+
def vacuum(self, db: str, *, analyze: bool = True, full: bool = False) -> None:
|
|
289
|
+
"""``VACUUM`` the target database (autocommit; cannot run in a transaction block)."""
|
|
290
|
+
opts = []
|
|
291
|
+
if full:
|
|
292
|
+
opts.append("FULL")
|
|
293
|
+
if analyze:
|
|
294
|
+
opts.append("ANALYZE")
|
|
295
|
+
stmt = "VACUUM" + (f" ({', '.join(opts)})" if opts else "")
|
|
296
|
+
self._exec_in(db, stmt)
|
|
297
|
+
|
|
298
|
+
def analyze(self, db: str) -> None:
|
|
299
|
+
self._exec_in(db, "ANALYZE")
|
|
300
|
+
|
|
301
|
+
def reindex(self, db: str) -> None:
|
|
302
|
+
"""``REINDEX DATABASE`` the target database."""
|
|
303
|
+
self._exec_in(db, f"REINDEX DATABASE {quote_identifier(db)}")
|
|
304
|
+
|
|
305
|
+
def _exec_in(self, db: str, *statements: str) -> None:
|
|
306
|
+
"""Run autocommit statements against ``db`` itself (not the maintenance db)."""
|
|
307
|
+
cfg = dict(self._config)
|
|
308
|
+
cfg["dbname"] = db
|
|
309
|
+
engine = postgres.initialize(config=cfg)
|
|
310
|
+
|
|
311
|
+
@engine.transaction
|
|
312
|
+
def _run(tx):
|
|
313
|
+
tx.rollback()
|
|
314
|
+
tx.connection.autocommit = True
|
|
315
|
+
for sql in statements:
|
|
316
|
+
if sql:
|
|
317
|
+
tx.execute(sql)
|
|
318
|
+
|
|
319
|
+
_run()
|
|
@@ -34,6 +34,7 @@ src/velocity/aws/tests/test_lambda_handler_json_serialization.py
|
|
|
34
34
|
src/velocity/aws/tests/test_response.py
|
|
35
35
|
src/velocity/db/__init__.py
|
|
36
36
|
src/velocity/db/exceptions.py
|
|
37
|
+
src/velocity/db/maintenance.py
|
|
37
38
|
src/velocity/db/migrations.py
|
|
38
39
|
src/velocity/db/utils.py
|
|
39
40
|
src/velocity/db/core/__init__.py
|
|
@@ -162,9 +163,11 @@ tests/test_connection_pool.py
|
|
|
162
163
|
tests/test_connection_resilience.py
|
|
163
164
|
tests/test_context_job_descriptions.py
|
|
164
165
|
tests/test_db_credentials_ssm_cascade.py
|
|
166
|
+
tests/test_db_maintenance.py
|
|
165
167
|
tests/test_decorators.py
|
|
166
168
|
tests/test_dirty_pipeline_fast_path.py
|
|
167
169
|
tests/test_email_processing.py
|
|
170
|
+
tests/test_enqueue_send_failures.py
|
|
168
171
|
tests/test_get_cognito_user_provider.py
|
|
169
172
|
tests/test_http_handler_rollback.py
|
|
170
173
|
tests/test_iconv_money_to_cents.py
|