velocity-python 0.1.75__tar.gz → 0.1.77__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.75 → velocity_python-0.1.77}/PKG-INFO +1 -1
- {velocity_python-0.1.75 → velocity_python-0.1.77}/pyproject.toml +1 -1
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.77/src/velocity/db/maintenance.py +329 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/SOURCES.txt +2 -0
- velocity_python-0.1.77/tests/test_db_maintenance.py +198 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/LICENSE +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/README.md +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/setup.cfg +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_observability.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.75 → velocity_python-0.1.77}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
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 _engine_config(self, dbname: str) -> dict:
|
|
76
|
+
"""Connection config that connects to ``dbname``.
|
|
77
|
+
|
|
78
|
+
The source/destination database is operated *on*, never connected *to*, so
|
|
79
|
+
any caller-supplied database name is dropped and replaced. (libpq uses
|
|
80
|
+
``dbname``; ``postgres.initialize`` remaps a stray ``database`` key, which
|
|
81
|
+
would otherwise override the forced connection database.)
|
|
82
|
+
"""
|
|
83
|
+
cfg = dict(self._config)
|
|
84
|
+
cfg.pop("database", None)
|
|
85
|
+
cfg.pop("dbname", None)
|
|
86
|
+
cfg["dbname"] = dbname
|
|
87
|
+
return cfg
|
|
88
|
+
|
|
89
|
+
def _get_engine(self):
|
|
90
|
+
if self._engine is None:
|
|
91
|
+
self._engine = postgres.initialize(config=self._engine_config(self.maintenance_db))
|
|
92
|
+
return self._engine
|
|
93
|
+
|
|
94
|
+
def _exec(self, *statements: str) -> None:
|
|
95
|
+
"""Run DDL/admin statements in autocommit (required for CREATE/DROP/RENAME DATABASE)."""
|
|
96
|
+
engine = self._get_engine()
|
|
97
|
+
|
|
98
|
+
@engine.transaction
|
|
99
|
+
def _run(tx):
|
|
100
|
+
tx.rollback() # discard the implicit transaction opened on checkout
|
|
101
|
+
tx.connection.autocommit = True
|
|
102
|
+
for sql in statements:
|
|
103
|
+
if sql:
|
|
104
|
+
tx.execute(sql)
|
|
105
|
+
|
|
106
|
+
_run()
|
|
107
|
+
|
|
108
|
+
def _query(self, sql: str) -> List[dict]:
|
|
109
|
+
engine = self._get_engine()
|
|
110
|
+
|
|
111
|
+
@engine.transaction
|
|
112
|
+
def _run(tx):
|
|
113
|
+
return list(tx.execute(sql).as_dict())
|
|
114
|
+
|
|
115
|
+
return _run()
|
|
116
|
+
|
|
117
|
+
def _conn_params(self) -> dict:
|
|
118
|
+
return {
|
|
119
|
+
"host": self._config.get("host") or os.environ.get("DBHost"),
|
|
120
|
+
"port": self._config.get("port") or os.environ.get("DBPort"),
|
|
121
|
+
"user": self._config.get("user") or os.environ.get("DBUser"),
|
|
122
|
+
"password": self._config.get("password") or os.environ.get("DBPassword"),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------ guards
|
|
126
|
+
def _guard(self, db: str, action: str) -> None:
|
|
127
|
+
if db in self.protected:
|
|
128
|
+
raise PermissionError(
|
|
129
|
+
f"Refusing to {action} protected database {db!r}. "
|
|
130
|
+
"Construct with protected=... to override."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------ SQL helpers
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _terminate_sql(db: str) -> str:
|
|
136
|
+
return (
|
|
137
|
+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
|
|
138
|
+
f"WHERE datname = {quote_literal(db)} AND pid <> pg_backend_pid()"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------ queries
|
|
142
|
+
def exists(self, db: str) -> bool:
|
|
143
|
+
return bool(
|
|
144
|
+
self._query(f"SELECT 1 AS x FROM pg_database WHERE datname = {quote_literal(db)}")
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def list_databases(self, *, pattern: Optional[str] = None) -> List[str]:
|
|
148
|
+
where = "WHERE datistemplate = false"
|
|
149
|
+
if pattern:
|
|
150
|
+
where += f" AND datname LIKE {quote_literal(pattern)}"
|
|
151
|
+
rows = self._query(f"SELECT datname FROM pg_database {where} ORDER BY datname")
|
|
152
|
+
return [r["datname"] for r in rows]
|
|
153
|
+
|
|
154
|
+
def list_snapshots(self, db: str) -> List[str]:
|
|
155
|
+
"""Snapshot databases for ``db``, oldest first (sortable timestamp suffix)."""
|
|
156
|
+
return sorted(self.list_databases(pattern=f"{db}{SNAPSHOT_INFIX}%"))
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------ connection control
|
|
159
|
+
def terminate_connections(self, db: str) -> None:
|
|
160
|
+
self._exec(self._terminate_sql(db))
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------ core ops
|
|
163
|
+
def clone(self, source: str, dest: str, *, drop_existing: bool = False) -> str:
|
|
164
|
+
"""``CREATE DATABASE dest WITH TEMPLATE source`` (server-side, near-instant)."""
|
|
165
|
+
self._guard(dest, "overwrite")
|
|
166
|
+
if self.exists(dest):
|
|
167
|
+
if not drop_existing:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Destination {dest!r} already exists. Pass drop_existing=True to replace it."
|
|
170
|
+
)
|
|
171
|
+
self.drop(dest)
|
|
172
|
+
# A template clone requires no other sessions on the source.
|
|
173
|
+
self._exec(
|
|
174
|
+
self._terminate_sql(source),
|
|
175
|
+
f"CREATE DATABASE {quote_identifier(dest)} WITH TEMPLATE {quote_identifier(source)}",
|
|
176
|
+
)
|
|
177
|
+
return dest
|
|
178
|
+
|
|
179
|
+
def snapshot(self, db: str, *, label: Optional[str] = None) -> str:
|
|
180
|
+
"""Clone ``db`` to ``<db>-snapshot-<ts>`` (or a custom ``label``)."""
|
|
181
|
+
dest = f"{db}{SNAPSHOT_INFIX}{label or timestamp()}"
|
|
182
|
+
return self.clone(db, dest)
|
|
183
|
+
|
|
184
|
+
def rename(self, old: str, new: str) -> str:
|
|
185
|
+
self._guard(old, "rename")
|
|
186
|
+
self._exec(
|
|
187
|
+
self._terminate_sql(old),
|
|
188
|
+
f"ALTER DATABASE {quote_identifier(old)} RENAME TO {quote_identifier(new)}",
|
|
189
|
+
)
|
|
190
|
+
return new
|
|
191
|
+
|
|
192
|
+
def drop(self, db: str, *, if_exists: bool = True) -> None:
|
|
193
|
+
self._guard(db, "drop")
|
|
194
|
+
clause = "DROP DATABASE IF EXISTS" if if_exists else "DROP DATABASE"
|
|
195
|
+
self._exec(self._terminate_sql(db), f"{clause} {quote_identifier(db)}")
|
|
196
|
+
|
|
197
|
+
def refresh(self, source: str, dest: str, *, keep_snapshot: bool = True) -> str:
|
|
198
|
+
"""Replace ``dest`` with a fresh template-clone of ``source``.
|
|
199
|
+
|
|
200
|
+
Safe sequence: terminate ``dest`` sessions -> rename ``dest`` to
|
|
201
|
+
``dest-snapshot-<ts>`` (or drop it when ``keep_snapshot=False``) -> clone
|
|
202
|
+
``source`` -> ``dest``. Keeping the snapshot makes the refresh reversible.
|
|
203
|
+
"""
|
|
204
|
+
self._guard(dest, "overwrite")
|
|
205
|
+
if self.exists(dest):
|
|
206
|
+
if keep_snapshot:
|
|
207
|
+
self.rename(dest, f"{dest}{SNAPSHOT_INFIX}{timestamp()}")
|
|
208
|
+
else:
|
|
209
|
+
self.drop(dest)
|
|
210
|
+
return self.clone(source, dest)
|
|
211
|
+
|
|
212
|
+
def prune_snapshots(
|
|
213
|
+
self,
|
|
214
|
+
db: str,
|
|
215
|
+
*,
|
|
216
|
+
keep: int = 3,
|
|
217
|
+
older_than_days: Optional[int] = None,
|
|
218
|
+
dry_run: bool = False,
|
|
219
|
+
) -> List[str]:
|
|
220
|
+
"""Drop old snapshot databases for ``db``.
|
|
221
|
+
|
|
222
|
+
With ``older_than_days`` set, drops snapshots whose timestamp is older than
|
|
223
|
+
the cutoff. Otherwise keeps the ``keep`` newest and drops the rest. Returns
|
|
224
|
+
the names dropped (or that would be dropped, when ``dry_run=True``).
|
|
225
|
+
"""
|
|
226
|
+
snaps = self.list_snapshots(db) # ascending (oldest first)
|
|
227
|
+
to_drop: List[str] = []
|
|
228
|
+
if older_than_days is not None:
|
|
229
|
+
cutoff = datetime.datetime.now() - datetime.timedelta(days=older_than_days)
|
|
230
|
+
prefix = f"{db}{SNAPSHOT_INFIX}"
|
|
231
|
+
for name in snaps:
|
|
232
|
+
suffix = name[len(prefix):]
|
|
233
|
+
try:
|
|
234
|
+
when = datetime.datetime.strptime(suffix, TS_FORMAT)
|
|
235
|
+
except ValueError:
|
|
236
|
+
continue # unrecognized suffix -> leave it alone
|
|
237
|
+
if when < cutoff:
|
|
238
|
+
to_drop.append(name)
|
|
239
|
+
elif keep >= 0 and len(snaps) > keep:
|
|
240
|
+
to_drop = snaps[: len(snaps) - keep]
|
|
241
|
+
|
|
242
|
+
if not dry_run:
|
|
243
|
+
for name in to_drop:
|
|
244
|
+
self.drop(name)
|
|
245
|
+
return to_drop
|
|
246
|
+
|
|
247
|
+
# ------------------------------------------------------------------ portable backup/restore
|
|
248
|
+
def backup_to_file(
|
|
249
|
+
self, db: str, path: str, *, fmt: str = "custom", extra_args: Iterable[str] = ()
|
|
250
|
+
) -> str:
|
|
251
|
+
"""Portable off-instance backup via ``pg_dump`` (use for cross-server moves;
|
|
252
|
+
prefer ``snapshot``/``clone`` for same-server copies). ``extra_args`` are
|
|
253
|
+
passed through to ``pg_dump`` (e.g. ``("-O",)`` for no-owner)."""
|
|
254
|
+
if fmt not in _BACKUP_FORMAT_FLAGS:
|
|
255
|
+
raise ValueError(f"Unknown fmt {fmt!r}; choose one of {sorted(_BACKUP_FORMAT_FLAGS)}.")
|
|
256
|
+
p = self._conn_params()
|
|
257
|
+
self._run_tool(
|
|
258
|
+
["pg_dump", "-h", str(p["host"]), "-p", str(p["port"]), "-U", str(p["user"]),
|
|
259
|
+
_BACKUP_FORMAT_FLAGS[fmt], *extra_args, "-f", path, db]
|
|
260
|
+
)
|
|
261
|
+
return path
|
|
262
|
+
|
|
263
|
+
def restore_from_file(
|
|
264
|
+
self,
|
|
265
|
+
path: str,
|
|
266
|
+
dest: str,
|
|
267
|
+
*,
|
|
268
|
+
fmt: str = "custom",
|
|
269
|
+
drop_existing: bool = False,
|
|
270
|
+
extra_args: Iterable[str] = (),
|
|
271
|
+
) -> str:
|
|
272
|
+
"""Restore a ``pg_dump`` file into a freshly created ``dest`` database.
|
|
273
|
+
``extra_args`` are passed to ``pg_restore``/``psql``."""
|
|
274
|
+
self._guard(dest, "overwrite")
|
|
275
|
+
if self.exists(dest):
|
|
276
|
+
if not drop_existing:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Destination {dest!r} already exists. Pass drop_existing=True to replace it."
|
|
279
|
+
)
|
|
280
|
+
self.drop(dest)
|
|
281
|
+
self._exec(f"CREATE DATABASE {quote_identifier(dest)}")
|
|
282
|
+
p = self._conn_params()
|
|
283
|
+
base = ["-h", str(p["host"]), "-p", str(p["port"]), "-U", str(p["user"])]
|
|
284
|
+
if fmt == "plain":
|
|
285
|
+
self._run_tool(["psql", *base, *extra_args, "-d", dest, "-f", path])
|
|
286
|
+
else:
|
|
287
|
+
self._run_tool(["pg_restore", *base, *extra_args, "-d", dest, path])
|
|
288
|
+
return dest
|
|
289
|
+
|
|
290
|
+
def _run_tool(self, cmd: List[str]) -> None:
|
|
291
|
+
import subprocess
|
|
292
|
+
|
|
293
|
+
env = dict(os.environ)
|
|
294
|
+
password = self._conn_params().get("password")
|
|
295
|
+
if password:
|
|
296
|
+
env["PGPASSWORD"] = str(password)
|
|
297
|
+
subprocess.run(cmd, check=True, env=env, capture_output=True, text=True)
|
|
298
|
+
|
|
299
|
+
# ------------------------------------------------------------------ maintenance
|
|
300
|
+
def vacuum(self, db: str, *, analyze: bool = True, full: bool = False) -> None:
|
|
301
|
+
"""``VACUUM`` the target database (autocommit; cannot run in a transaction block)."""
|
|
302
|
+
opts = []
|
|
303
|
+
if full:
|
|
304
|
+
opts.append("FULL")
|
|
305
|
+
if analyze:
|
|
306
|
+
opts.append("ANALYZE")
|
|
307
|
+
stmt = "VACUUM" + (f" ({', '.join(opts)})" if opts else "")
|
|
308
|
+
self._exec_in(db, stmt)
|
|
309
|
+
|
|
310
|
+
def analyze(self, db: str) -> None:
|
|
311
|
+
self._exec_in(db, "ANALYZE")
|
|
312
|
+
|
|
313
|
+
def reindex(self, db: str) -> None:
|
|
314
|
+
"""``REINDEX DATABASE`` the target database."""
|
|
315
|
+
self._exec_in(db, f"REINDEX DATABASE {quote_identifier(db)}")
|
|
316
|
+
|
|
317
|
+
def _exec_in(self, db: str, *statements: str) -> None:
|
|
318
|
+
"""Run autocommit statements against ``db`` itself (not the maintenance db)."""
|
|
319
|
+
engine = postgres.initialize(config=self._engine_config(db))
|
|
320
|
+
|
|
321
|
+
@engine.transaction
|
|
322
|
+
def _run(tx):
|
|
323
|
+
tx.rollback()
|
|
324
|
+
tx.connection.autocommit = True
|
|
325
|
+
for sql in statements:
|
|
326
|
+
if sql:
|
|
327
|
+
tx.execute(sql)
|
|
328
|
+
|
|
329
|
+
_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,6 +163,7 @@ 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
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from velocity.db import maintenance
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _RecordingMaintenance(maintenance.DatabaseMaintenance):
|
|
7
|
+
"""In-memory model: records emitted SQL and tracks database existence so the
|
|
8
|
+
internal exists()/drop()/rename() checks stay consistent -- no live DB."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, *, existing=(), snapshots=(), **kwargs):
|
|
11
|
+
super().__init__(**kwargs)
|
|
12
|
+
self._existing = set(existing)
|
|
13
|
+
self._snapshots = list(snapshots)
|
|
14
|
+
self.executed = []
|
|
15
|
+
|
|
16
|
+
def _exec(self, *statements):
|
|
17
|
+
self.executed.extend(s for s in statements if s)
|
|
18
|
+
|
|
19
|
+
def exists(self, db):
|
|
20
|
+
return db in self._existing
|
|
21
|
+
|
|
22
|
+
def list_snapshots(self, db):
|
|
23
|
+
return sorted(self._snapshots)
|
|
24
|
+
|
|
25
|
+
# state-tracking wrappers (call through to the real logic, then update state)
|
|
26
|
+
def clone(self, source, dest, **kwargs):
|
|
27
|
+
result = super().clone(source, dest, **kwargs)
|
|
28
|
+
self._existing.add(dest)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
def drop(self, db, **kwargs):
|
|
32
|
+
super().drop(db, **kwargs)
|
|
33
|
+
self._existing.discard(db)
|
|
34
|
+
|
|
35
|
+
def rename(self, old, new):
|
|
36
|
+
result = super().rename(old, new)
|
|
37
|
+
if old in self._existing:
|
|
38
|
+
self._existing.discard(old)
|
|
39
|
+
self._existing.add(new)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def sql(self):
|
|
44
|
+
return " | ".join(self.executed)
|
|
45
|
+
|
|
46
|
+
def _index(self, needle):
|
|
47
|
+
return next(i for i, s in enumerate(self.executed) if needle in s)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestClone(unittest.TestCase):
|
|
51
|
+
def test_emits_terminate_then_template(self):
|
|
52
|
+
m = _RecordingMaintenance()
|
|
53
|
+
m.clone("src", "dst")
|
|
54
|
+
self.assertIn("pg_terminate_backend", m.sql)
|
|
55
|
+
self.assertIn('CREATE DATABASE "dst" WITH TEMPLATE "src"', m.sql)
|
|
56
|
+
# source connections are terminated BEFORE the template clone
|
|
57
|
+
self.assertLess(self.m_terminate_src(m), m._index("CREATE DATABASE"))
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def m_terminate_src(m):
|
|
61
|
+
return next(i for i, s in enumerate(m.executed) if "pg_terminate" in s and "'src'" in s)
|
|
62
|
+
|
|
63
|
+
def test_existing_without_drop_raises(self):
|
|
64
|
+
m = _RecordingMaintenance(existing={"dst"})
|
|
65
|
+
with self.assertRaises(ValueError):
|
|
66
|
+
m.clone("src", "dst")
|
|
67
|
+
|
|
68
|
+
def test_existing_with_drop_replaces(self):
|
|
69
|
+
m = _RecordingMaintenance(existing={"dst"})
|
|
70
|
+
m.clone("src", "dst", drop_existing=True)
|
|
71
|
+
self.assertIn('DROP DATABASE IF EXISTS "dst"', m.sql)
|
|
72
|
+
self.assertIn('CREATE DATABASE "dst" WITH TEMPLATE "src"', m.sql)
|
|
73
|
+
self.assertLess(m._index("DROP DATABASE"), m._index("CREATE DATABASE"))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestRefresh(unittest.TestCase):
|
|
77
|
+
def test_keeps_snapshot_then_clones(self):
|
|
78
|
+
m = _RecordingMaintenance(existing={"dev"})
|
|
79
|
+
m.refresh("prod", "dev")
|
|
80
|
+
self.assertRegex(
|
|
81
|
+
m.sql, r'ALTER DATABASE "dev" RENAME TO "dev-snapshot-\d{8}-\d{6}"'
|
|
82
|
+
)
|
|
83
|
+
self.assertIn('CREATE DATABASE "dev" WITH TEMPLATE "prod"', m.sql)
|
|
84
|
+
self.assertLess(m._index("RENAME TO"), m._index("CREATE DATABASE"))
|
|
85
|
+
|
|
86
|
+
def test_no_snapshot_drops(self):
|
|
87
|
+
m = _RecordingMaintenance(existing={"dev"})
|
|
88
|
+
m.refresh("prod", "dev", keep_snapshot=False)
|
|
89
|
+
self.assertIn('DROP DATABASE IF EXISTS "dev"', m.sql)
|
|
90
|
+
self.assertNotIn("RENAME TO", m.sql)
|
|
91
|
+
self.assertIn('CREATE DATABASE "dev" WITH TEMPLATE "prod"', m.sql)
|
|
92
|
+
|
|
93
|
+
def test_dest_absent_just_clones(self):
|
|
94
|
+
m = _RecordingMaintenance(existing=set())
|
|
95
|
+
m.refresh("prod", "dev")
|
|
96
|
+
self.assertNotIn("RENAME TO", m.sql)
|
|
97
|
+
self.assertNotIn("DROP DATABASE", m.sql)
|
|
98
|
+
self.assertIn('CREATE DATABASE "dev" WITH TEMPLATE "prod"', m.sql)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestSnapshotAndPrune(unittest.TestCase):
|
|
102
|
+
def test_snapshot_custom_label(self):
|
|
103
|
+
m = _RecordingMaintenance()
|
|
104
|
+
name = m.snapshot("dev", label="manual")
|
|
105
|
+
self.assertEqual(name, "dev-snapshot-manual")
|
|
106
|
+
self.assertIn('CREATE DATABASE "dev-snapshot-manual" WITH TEMPLATE "dev"', m.sql)
|
|
107
|
+
|
|
108
|
+
def test_prune_keeps_newest(self):
|
|
109
|
+
snaps = [
|
|
110
|
+
"dev-snapshot-20260101-000000",
|
|
111
|
+
"dev-snapshot-20260201-000000",
|
|
112
|
+
"dev-snapshot-20260301-000000",
|
|
113
|
+
]
|
|
114
|
+
m = _RecordingMaintenance(snapshots=snaps)
|
|
115
|
+
dropped = m.prune_snapshots("dev", keep=1)
|
|
116
|
+
self.assertEqual(dropped, snaps[:2])
|
|
117
|
+
self.assertIn('DROP DATABASE IF EXISTS "dev-snapshot-20260101-000000"', m.sql)
|
|
118
|
+
self.assertNotIn("20260301", m.sql) # newest is kept
|
|
119
|
+
|
|
120
|
+
def test_prune_dry_run_executes_nothing(self):
|
|
121
|
+
snaps = ["dev-snapshot-20260101-000000", "dev-snapshot-20260201-000000"]
|
|
122
|
+
m = _RecordingMaintenance(snapshots=snaps)
|
|
123
|
+
dropped = m.prune_snapshots("dev", keep=1, dry_run=True)
|
|
124
|
+
self.assertEqual(dropped, snaps[:1])
|
|
125
|
+
self.assertEqual(m.executed, [])
|
|
126
|
+
|
|
127
|
+
def test_prune_older_than_days(self):
|
|
128
|
+
snaps = ["dev-snapshot-20200101-000000", "dev-snapshot-29990101-000000"]
|
|
129
|
+
m = _RecordingMaintenance(snapshots=snaps)
|
|
130
|
+
dropped = m.prune_snapshots("dev", older_than_days=30)
|
|
131
|
+
self.assertEqual(dropped, ["dev-snapshot-20200101-000000"])
|
|
132
|
+
|
|
133
|
+
def test_prune_keep_all_when_under_limit(self):
|
|
134
|
+
snaps = ["dev-snapshot-20260101-000000"]
|
|
135
|
+
m = _RecordingMaintenance(snapshots=snaps)
|
|
136
|
+
self.assertEqual(m.prune_snapshots("dev", keep=3), [])
|
|
137
|
+
self.assertEqual(m.executed, [])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestGuards(unittest.TestCase):
|
|
141
|
+
def test_protected_blocks_drop(self):
|
|
142
|
+
m = _RecordingMaintenance(existing={"caringcent-production"})
|
|
143
|
+
with self.assertRaises(PermissionError):
|
|
144
|
+
m.drop("caringcent-production")
|
|
145
|
+
|
|
146
|
+
def test_protected_blocks_overwrite_via_clone(self):
|
|
147
|
+
m = _RecordingMaintenance()
|
|
148
|
+
with self.assertRaises(PermissionError):
|
|
149
|
+
m.clone("x", "caringcent-production")
|
|
150
|
+
|
|
151
|
+
def test_protected_blocks_refresh_dest(self):
|
|
152
|
+
m = _RecordingMaintenance()
|
|
153
|
+
with self.assertRaises(PermissionError):
|
|
154
|
+
m.refresh("x", "caringcent-production")
|
|
155
|
+
|
|
156
|
+
def test_protected_blocks_rename(self):
|
|
157
|
+
m = _RecordingMaintenance(existing={"caringcent-production"})
|
|
158
|
+
with self.assertRaises(PermissionError):
|
|
159
|
+
m.rename("caringcent-production", "x")
|
|
160
|
+
|
|
161
|
+
def test_override_protected_allows(self):
|
|
162
|
+
m = _RecordingMaintenance(existing={"caringcent-production"}, protected=())
|
|
163
|
+
m.drop("caringcent-production")
|
|
164
|
+
self.assertIn('DROP DATABASE IF EXISTS "caringcent-production"', m.sql)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestQuoting(unittest.TestCase):
|
|
168
|
+
def test_identifier_quoting_doubles_double_quotes(self):
|
|
169
|
+
self.assertEqual(maintenance.quote_identifier('a"b'), '"a""b"')
|
|
170
|
+
|
|
171
|
+
def test_literal_quoting_doubles_single_quotes(self):
|
|
172
|
+
self.assertEqual(maintenance.quote_literal("a'b"), "'a''b'")
|
|
173
|
+
|
|
174
|
+
def test_terminate_sql_escapes_name(self):
|
|
175
|
+
sql = maintenance.DatabaseMaintenance._terminate_sql("o'brien")
|
|
176
|
+
self.assertIn("datname = 'o''brien'", sql)
|
|
177
|
+
self.assertIn("pid <> pg_backend_pid()", sql)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestEngineConfig(unittest.TestCase):
|
|
181
|
+
def test_forces_maintenance_db_and_drops_caller_db_name(self):
|
|
182
|
+
m = maintenance.DatabaseMaintenance(
|
|
183
|
+
config={"database": "caringcent-develop", "host": "h", "user": "u"}
|
|
184
|
+
)
|
|
185
|
+
cfg = m._engine_config(m.maintenance_db)
|
|
186
|
+
self.assertEqual(cfg["dbname"], "postgres")
|
|
187
|
+
self.assertNotIn("database", cfg) # the stray key that would override dbname is gone
|
|
188
|
+
self.assertEqual(cfg["host"], "h")
|
|
189
|
+
self.assertEqual(cfg["user"], "u")
|
|
190
|
+
|
|
191
|
+
def test_target_db_overrides_caller_dbname(self):
|
|
192
|
+
m = maintenance.DatabaseMaintenance(config={"dbname": "x", "host": "h"})
|
|
193
|
+
cfg = m._engine_config("mydb")
|
|
194
|
+
self.assertEqual(cfg["dbname"], "mydb")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/context_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/lambda_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/__init__.py
RENAMED
|
File without changes
|
{velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
File without changes
|
{velocity_python-0.1.75 → velocity_python-0.1.77}/src/velocity/aws/handlers/mixins/web_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|