velocity-python 0.2.8__tar.gz → 0.2.9__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.2.8/src/velocity_python.egg-info → velocity_python-0.2.9}/PKG-INFO +1 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/pyproject.toml +1 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/__init__.py +1 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/authz.py +48 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/security.py +105 -29
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/data_service.py +35 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/tiles_actions.py +8 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/schema.py +3 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_ensure_infrastructure.py +1 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_security_capability.py +77 -0
- velocity_python-0.2.9/tests/test_security_events_table_gate.py +210 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/LICENSE +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/README.md +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/setup.cfg +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/base.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/dispatch.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/infra.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/jobs.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/provisioning.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/retention.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/capabilities/single_session.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/ci_gate.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/frontend_checks.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/facets.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/failures.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/feature_flags.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/action_names.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/audit_trail.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/client_errors.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/client_input.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/permissions_actions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/row_scoped.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/identity.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/job_lifecycle.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/ops.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/permissions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/queue_defaults.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_deploy_least_privilege.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_deploy_policy_size.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/tiles.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/user_data.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/lazy_engine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/fragments.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/tunnel.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/logging.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/_mirror_core.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/money.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/plaid_api.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/plaid_webhook.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/redact.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/schema.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/testing/__init__.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/testing/fake_db.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_access_error_403.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_action_names.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_amplify_redirects.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_asset_references.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_assets_service.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_async_support.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_audit_mixin.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_authz.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_aws_package_hygiene.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_blob_store.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_business_objects.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_capabilities.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_capability_catalog.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_check_changelog.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_ci_gate.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_client_errors_mixin.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_client_input_contract_export.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_client_sql_boundary.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_client_table_gate_case_and_pointers.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_cognito_client_lazy.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_cognito_user_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_context_flags_loader.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_context_queue_job.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_conv_combine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_owner_scope.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_row_number.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_data_service_security_gates.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_db_tunnel.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_declared_column_types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_decorators.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_deploy_config_fields.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_deploy_engine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_deploy_frontend_checks.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_deploy_operations_queue.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_dlq_constant.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_docs_snippets.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_email_processing.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_enqueue_metric_namespace.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_export_workbook.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_facets.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_facets_and_where.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_failures.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_feature_flags.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_identity.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_job_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_job_lifecycle.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_job_results.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_json_columns.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_lambda_handler_user_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_lazy_engine.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_merge_on_conflict.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_mirror_upsert_public.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_observability.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_on_action_default.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_on_commit.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_ops.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_audit_fixes.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_business_agnostic.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_charge_reference.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_extra_bounds.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_lazy_imports.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pdf.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_permissions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_permissions_actions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_permissions_notices.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_permissions_review.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_bulk_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_capability_types.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_ensure_infrastructure.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_get_tables.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_permissions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_pointer_joins.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_row_scope.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_search_export.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_tiles.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pgserver_user_data.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_plaid_api.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_plaid_webhook.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_pointer_join_sql.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_postgres_initializer_config.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_postgres_py_type.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_query_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_raw_sql_prefix_policy.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_read_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_readme_imports.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_response.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_result_returning.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_row_scoped_data_service.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_s3_bucket_config.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_s3_list_client_files.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_s3_upload.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_schema_lock_config_other_backends.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_search_paging_export.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_session_control.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_single_session_capability.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_sms_twilio_signature.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_ssm_config_require.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_table_alter.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_table_claim.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_table_find_cache.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_table_purge.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_testing_fake_db.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_tiles.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_tx_lifecycle_retry.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_tx_read_cache_and_bulk.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_user_data.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_user_not_provisioned.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_write_hook_create_flow.py +0 -0
- {velocity_python-0.2.8 → velocity_python-0.2.9}/tests/test_write_hook_empty_strings.py +0 -0
|
@@ -935,6 +935,33 @@ def is_protected(table_name):
|
|
|
935
935
|
return table_name in _PROTECTED
|
|
936
936
|
|
|
937
937
|
|
|
938
|
+
# Tables whose rows are confidential (the security audit: everyone's sign-ins,
|
|
939
|
+
# addresses, refusals). The generic Data actions READ them only on a handler
|
|
940
|
+
# that lists the table in ``confidential_reads`` (an explicit opt-in, e.g. an
|
|
941
|
+
# operators-only console), in every mode: ``gate`` records nothing, so even a
|
|
942
|
+
# permitted read there would be unaudited.
|
|
943
|
+
_CONFIDENTIAL = set()
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def confidential_tables(*names):
|
|
947
|
+
"""Mark tables generic Data reads refuse unless the handler opts in (see
|
|
948
|
+
``DataServiceMixin.confidential_reads``), whatever ``authz_mode`` says.
|
|
949
|
+
Combine with :func:`protect_tables` to refuse writes as well."""
|
|
950
|
+
_CONFIDENTIAL.update(name for name in names if isinstance(name, str) and name)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def is_confidential(table_name):
|
|
954
|
+
return table_name in _CONFIDENTIAL
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# velocity's own audit sink (capabilities.security.EVENTS_TABLE): append-only
|
|
958
|
+
# and confidential on every site, registered here so the rule never depends on
|
|
959
|
+
# which modules a handler happened to import (VelocityAudit SEC-12 follow-up).
|
|
960
|
+
SECURITY_EVENTS_TABLE = "sys_security_events"
|
|
961
|
+
protect_tables(SECURITY_EVENTS_TABLE)
|
|
962
|
+
confidential_tables(SECURITY_EVENTS_TABLE)
|
|
963
|
+
|
|
964
|
+
|
|
938
965
|
# ── the action index ───────────────────────────────────────────────────────
|
|
939
966
|
|
|
940
967
|
# id(rules tuple) -> (the tuple, {key: its rules in order}), where key is the
|
|
@@ -1319,6 +1346,15 @@ def _describe(resource):
|
|
|
1319
1346
|
return f"{resource.type}:{resource.id}"
|
|
1320
1347
|
|
|
1321
1348
|
|
|
1349
|
+
def record_confidential_read(handler, tx, context, table):
|
|
1350
|
+
"""Record one generic read of a confidential table by a handler that opted
|
|
1351
|
+
in (``confidential_reads``) as ``privileged_access``; False when it could
|
|
1352
|
+
not be recorded (the caller refuses the read). The opt-in serves an
|
|
1353
|
+
operators-only console; every use of it is still on the record."""
|
|
1354
|
+
decision = Decision(True, rule="confidential_reads", audit=True, action=DATA_READ)
|
|
1355
|
+
return bool(_record(handler, tx, context, decision, Resource(TABLE, table)))
|
|
1356
|
+
|
|
1357
|
+
|
|
1322
1358
|
def _record(handler, tx, context, decision, resource):
|
|
1323
1359
|
# imported here: the capabilities package pulls in more than a pure
|
|
1324
1360
|
# decision module should load at import
|
|
@@ -1330,6 +1366,16 @@ def _record(handler, tx, context, decision, resource):
|
|
|
1330
1366
|
if isinstance(session.get("masquerade"), dict)
|
|
1331
1367
|
else None
|
|
1332
1368
|
) or session.get("email_address")
|
|
1369
|
+
# the subject API Gateway's authorizer verified (the real caller, also
|
|
1370
|
+
# under masquerade), and its issuer, so the record names an identity and
|
|
1371
|
+
# not only an email
|
|
1372
|
+
sub = session.get("sub") or None
|
|
1373
|
+
provider = session.get("authentication_provider")
|
|
1374
|
+
issuer = session.get("iss") or (
|
|
1375
|
+
"https://" + provider.split(",", 1)[0].strip()
|
|
1376
|
+
if isinstance(provider, str) and provider.strip()
|
|
1377
|
+
else None
|
|
1378
|
+
)
|
|
1333
1379
|
return record_event(
|
|
1334
1380
|
tx,
|
|
1335
1381
|
PRIVILEGED_ACCESS,
|
|
@@ -1337,4 +1383,6 @@ def _record(handler, tx, context, decision, resource):
|
|
|
1337
1383
|
aws_event=getattr(handler, "aws_event", None),
|
|
1338
1384
|
context=context,
|
|
1339
1385
|
user_email=actor,
|
|
1386
|
+
user_sub=sub,
|
|
1387
|
+
detail={"iss": issuer} if issuer else None,
|
|
1340
1388
|
)
|
|
@@ -20,6 +20,15 @@ Attach :class:`SecurityMixin` FIRST in a handler's bases; it runs in
|
|
|
20
20
|
``beforeAction`` ahead of auth and every action, and is a no-op unless the
|
|
21
21
|
site's ``sys_capabilities`` row for ``security`` is enabled. A refusal raises
|
|
22
22
|
:class:`AccessError` (a 403) after committing a ``sys_security_events`` row.
|
|
23
|
+
|
|
24
|
+
``sys_security_events`` itself is a CORE table (``SCHEMA``, in
|
|
25
|
+
``velocity.aws.schema``'s ``core`` group), not the capability's: it is
|
|
26
|
+
velocity's one audit sink — refusals, masquerade grants, sign-ins — and every
|
|
27
|
+
site keeps it whether or not the guards are on (VelocityAudit SEC-12: with the
|
|
28
|
+
table capability-owned, a site without ``security`` silently dropped its
|
|
29
|
+
masquerade audit). Pruning stays here (``security_events_days``, default 365 to
|
|
30
|
+
match a one-year audit retention); a site without the capability lists the
|
|
31
|
+
table in the ``retention`` capability instead.
|
|
23
32
|
"""
|
|
24
33
|
|
|
25
34
|
from __future__ import annotations
|
|
@@ -46,6 +55,48 @@ log = logging.getLogger(__name__)
|
|
|
46
55
|
|
|
47
56
|
EVENTS_TABLE = "sys_security_events"
|
|
48
57
|
RATE_TABLE = "sys_rate_limit"
|
|
58
|
+
EVENT_COLUMNS = {
|
|
59
|
+
"guard": "TEXT",
|
|
60
|
+
"reason": "TEXT",
|
|
61
|
+
"ip": "TEXT",
|
|
62
|
+
"country": "TEXT",
|
|
63
|
+
"path": "TEXT",
|
|
64
|
+
"action": "TEXT",
|
|
65
|
+
"user_email": "TEXT",
|
|
66
|
+
"user_agent": "TEXT",
|
|
67
|
+
"user_sub": "TEXT",
|
|
68
|
+
"detail": "JSONB",
|
|
69
|
+
# when it happened (a sign-in trigger fired), which a queue can write
|
|
70
|
+
# later than sys_created; defaults to the time of writing
|
|
71
|
+
"occurred_at": "TIMESTAMPTZ",
|
|
72
|
+
}
|
|
73
|
+
# Append-only and confidential on every site: authz registers the table as
|
|
74
|
+
# protected (no generic write) and confidential (generic reads need an opt-in
|
|
75
|
+
# or enforce) itself, so the rule holds whether or not this module is imported.
|
|
76
|
+
# core infrastructure (velocity.aws.schema "core"): every site has the table
|
|
77
|
+
SCHEMA = {
|
|
78
|
+
EVENTS_TABLE: {
|
|
79
|
+
"columns": dict(EVENT_COLUMNS),
|
|
80
|
+
"indexes": [
|
|
81
|
+
{
|
|
82
|
+
"columns": ["sys_created"],
|
|
83
|
+
"name": "idx__sys_security_events__sys_created",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"columns": ["guard", "sys_created"],
|
|
87
|
+
"name": "idx__sys_security_events__guard",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"columns": ["user_sub", "sys_created"],
|
|
91
|
+
"name": "idx__sys_security_events__user_sub",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"columns": ["user_sub", "occurred_at"],
|
|
95
|
+
"name": "idx__sys_security_events__user_sub_occurred",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
49
100
|
CACHE_TTL_SECONDS = 60 # per-container cache of the switches + site row
|
|
50
101
|
|
|
51
102
|
SWITCHES = {
|
|
@@ -59,7 +110,7 @@ SWITCHES = {
|
|
|
59
110
|
KNOBS = {
|
|
60
111
|
"security_rate_limit_per_minute": "120",
|
|
61
112
|
"security_payload_max_bytes": "1000000",
|
|
62
|
-
"security_events_days": "
|
|
113
|
+
"security_events_days": "365",
|
|
63
114
|
}
|
|
64
115
|
|
|
65
116
|
|
|
@@ -275,30 +326,62 @@ def enforce(tx, aws_event, context, *, now=None):
|
|
|
275
326
|
return config
|
|
276
327
|
|
|
277
328
|
|
|
278
|
-
def record_event(
|
|
329
|
+
def record_event(
|
|
330
|
+
tx,
|
|
331
|
+
guard,
|
|
332
|
+
reason,
|
|
333
|
+
*,
|
|
334
|
+
aws_event=None,
|
|
335
|
+
context=None,
|
|
336
|
+
user_email=None,
|
|
337
|
+
user_sub=None,
|
|
338
|
+
detail=None,
|
|
339
|
+
occurred_at=None,
|
|
340
|
+
):
|
|
279
341
|
"""Append one ``sys_security_events`` row and commit it — an audit row
|
|
280
342
|
must survive whatever the caller does next (typically raise). The one
|
|
281
|
-
audit sink for the framework: the request guards, masquerade grants,
|
|
282
|
-
anything else security-relevant write here.
|
|
283
|
-
|
|
284
|
-
|
|
343
|
+
audit sink for the framework: the request guards, masquerade grants,
|
|
344
|
+
sign-ins, and anything else security-relevant write here. ``user_sub`` is
|
|
345
|
+
the identity provider's subject; ``detail`` a JSON-able dict of facts that
|
|
346
|
+
have no column; ``occurred_at`` when it happened, if not now (an event
|
|
347
|
+
relayed through a queue). Silent when the table is not provisioned in this site, and
|
|
348
|
+
an audit failure never breaks the request. Returns True when a row was
|
|
349
|
+
written.
|
|
350
|
+
|
|
351
|
+
A table created before ``user_sub``/``detail`` existed (a site whose schema
|
|
352
|
+
lane has not run since) still records the row without them: on a locked
|
|
353
|
+
schema an unknown column would otherwise lose the whole event."""
|
|
285
354
|
try:
|
|
286
355
|
table = tx.table(EVENTS_TABLE)
|
|
287
356
|
if not table.exists():
|
|
288
357
|
return False
|
|
289
358
|
facts = request_facts(aws_event)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
359
|
+
row = {
|
|
360
|
+
"guard": guard,
|
|
361
|
+
"reason": reason,
|
|
362
|
+
"ip": facts["ip"],
|
|
363
|
+
"country": facts["country"],
|
|
364
|
+
"path": facts["path"],
|
|
365
|
+
"action": _action(context) if context is not None else "",
|
|
366
|
+
"user_email": user_email,
|
|
367
|
+
"user_agent": facts["user_agent"][:512],
|
|
368
|
+
}
|
|
369
|
+
extra = {
|
|
370
|
+
"user_sub": user_sub,
|
|
371
|
+
"detail": detail,
|
|
372
|
+
"occurred_at": occurred_at or datetime.datetime.now(datetime.timezone.utc),
|
|
373
|
+
}
|
|
374
|
+
extra = {k: v for k, v in extra.items() if v is not None}
|
|
375
|
+
if extra:
|
|
376
|
+
try:
|
|
377
|
+
have = {c.lower() for c in table.sys_columns()}
|
|
378
|
+
except Exception:
|
|
379
|
+
# can't tell which newer columns exist: the event still lands
|
|
380
|
+
# with the columns every version of the table has
|
|
381
|
+
log.warning("security event %s: columns unknown", guard, exc_info=True)
|
|
382
|
+
have = set()
|
|
383
|
+
row.update({k: v for k, v in extra.items() if k in have})
|
|
384
|
+
table.insert(row)
|
|
302
385
|
tx.commit()
|
|
303
386
|
return True
|
|
304
387
|
except Exception:
|
|
@@ -386,16 +469,9 @@ class Security(Capability):
|
|
|
386
469
|
),
|
|
387
470
|
)
|
|
388
471
|
schema = {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
"ip": "TEXT",
|
|
393
|
-
"country": "TEXT",
|
|
394
|
-
"path": "TEXT",
|
|
395
|
-
"action": "TEXT",
|
|
396
|
-
"user_email": "TEXT",
|
|
397
|
-
"user_agent": "TEXT",
|
|
398
|
-
},
|
|
472
|
+
# the events table is core (module SCHEMA); listed here too so turning
|
|
473
|
+
# the capability on in an older site still ensures it
|
|
474
|
+
EVENTS_TABLE: dict(EVENT_COLUMNS),
|
|
399
475
|
RATE_TABLE: {"bucket": "TEXT", "window_start": "TIMESTAMPTZ", "hits": "BIGINT"},
|
|
400
476
|
}
|
|
401
477
|
schedules = (("prune", HOURLY),)
|
|
@@ -407,7 +483,7 @@ class Security(Capability):
|
|
|
407
483
|
if action != "prune":
|
|
408
484
|
raise ValueError(f"security: unknown action {action!r}")
|
|
409
485
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
410
|
-
days = int(config.get("security_events_days") or
|
|
486
|
+
days = int(config.get("security_events_days") or 365)
|
|
411
487
|
return {
|
|
412
488
|
"rate_buckets": tx.table(RATE_TABLE).purge(
|
|
413
489
|
{"<sys_created": now - datetime.timedelta(hours=1)}
|
{velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -588,6 +588,7 @@ class ExportStream:
|
|
|
588
588
|
def __enter__(self):
|
|
589
589
|
handler, payload = self.handler, self.payload
|
|
590
590
|
handler._assert_client_table(self.table, "obj")
|
|
591
|
+
handler._audit_confidential_read(self.tx, self.table, self.context)
|
|
591
592
|
# ``table`` is the only source: it is what passed the table gate and
|
|
592
593
|
# whose hooks and scope run below, so a payload ``obj`` naming any
|
|
593
594
|
# other table is refused rather than read.
|
|
@@ -915,6 +916,11 @@ class DataServiceMixin:
|
|
|
915
916
|
# handler, not in the gate methods' signatures, so a site's override of
|
|
916
917
|
# those keeps working.
|
|
917
918
|
_table_operation = authz.DATA_READ
|
|
919
|
+
# Confidential tables (``authz.confidential_tables``) this handler serves
|
|
920
|
+
# to generic reads: an explicit opt-in for a console whose every user is
|
|
921
|
+
# an operator. Without it such a table is never read through generic Data
|
|
922
|
+
# (in any authz mode); it is read through its own audited action.
|
|
923
|
+
confidential_reads = frozenset()
|
|
918
924
|
|
|
919
925
|
def _client_table_allowed(self, table):
|
|
920
926
|
# lower case only: velocity lower-cases every Table name and
|
|
@@ -937,10 +943,30 @@ class DataServiceMixin:
|
|
|
937
943
|
# ``authz_mode`` say.
|
|
938
944
|
if self._table_operation != authz.DATA_READ and authz.is_protected(table):
|
|
939
945
|
return False
|
|
946
|
+
# Confidential tables (``authz.confidential_tables``: the security
|
|
947
|
+
# audit) are read through generic Data only where the handler opted
|
|
948
|
+
# in. Never by default, and not under ``enforce`` either: ``gate``
|
|
949
|
+
# answers allow/deny but records nothing, so a super user's read would
|
|
950
|
+
# go unaudited; the table's own action (``authz.require``) records it.
|
|
951
|
+
if authz.is_confidential(table) and table not in self.confidential_reads:
|
|
952
|
+
return False
|
|
940
953
|
return authz.gate(
|
|
941
954
|
self, self._table_operation, authz.Resource(authz.TABLE, table)
|
|
942
955
|
)
|
|
943
956
|
|
|
957
|
+
def _audit_confidential_read(self, tx, table, context):
|
|
958
|
+
"""A generic read of a confidential table (only reachable on a handler
|
|
959
|
+
that lists it in ``confidential_reads``) is recorded as privileged
|
|
960
|
+
access first, and refused when it can't be: the opt-in keeps an
|
|
961
|
+
operators' grid working without making its reads invisible. Called by
|
|
962
|
+
the read ACTIONS right after their table gate (query, read-object,
|
|
963
|
+
find-object, query-direct, export), never from an overridable seam,
|
|
964
|
+
so a site's hook override can't skip it."""
|
|
965
|
+
if not authz.is_confidential(table):
|
|
966
|
+
return
|
|
967
|
+
if not authz.record_confidential_read(self, tx, context, table):
|
|
968
|
+
raise AccessError("That access could not be recorded, so it was refused.")
|
|
969
|
+
|
|
944
970
|
def _assert_client_table(self, table, label="table"):
|
|
945
971
|
"""The table gate every client action passes (SEC 4.2)."""
|
|
946
972
|
ClientInputGuard.table_name(table, label)
|
|
@@ -1022,7 +1048,11 @@ class DataServiceMixin:
|
|
|
1022
1048
|
if not info:
|
|
1023
1049
|
continue
|
|
1024
1050
|
foreign = info["referenced_table_name"]
|
|
1025
|
-
|
|
1051
|
+
# never into a confidential table: its reads are audited by the
|
|
1052
|
+
# actions that open it by name, and a join would skip that
|
|
1053
|
+
if authz.is_confidential(foreign) or not self._client_table_allowed(
|
|
1054
|
+
foreign
|
|
1055
|
+
):
|
|
1026
1056
|
raise AlertError(f"Access to '{table}.{local}>' is not permitted.")
|
|
1027
1057
|
# A join reads the foreign table's rows without that table's row
|
|
1028
1058
|
# filter. So someone who reaches only part of it cannot read it
|
|
@@ -2053,6 +2083,7 @@ class DataServiceMixin:
|
|
|
2053
2083
|
if not obj:
|
|
2054
2084
|
raise ValueError("Parameter 'object' cannot be empty")
|
|
2055
2085
|
self._assert_client_table(table_name, "tableName")
|
|
2086
|
+
self._audit_confidential_read(tx, table_name, context)
|
|
2056
2087
|
if not isinstance(obj, dict):
|
|
2057
2088
|
raise BadRequestError("Parameter 'object' must be an object")
|
|
2058
2089
|
if obj.get("sys_id") != NEW_ROW:
|
|
@@ -2094,6 +2125,7 @@ class DataServiceMixin:
|
|
|
2094
2125
|
if not query or "where" not in query:
|
|
2095
2126
|
raise ValueError("Parameter 'query' must contain 'where' clause")
|
|
2096
2127
|
self._assert_client_table(table_name, "tableName")
|
|
2128
|
+
self._audit_confidential_read(tx, table_name, context)
|
|
2097
2129
|
query["where"] = self._client_where(tx, table_name, query["where"])
|
|
2098
2130
|
|
|
2099
2131
|
row = self.find_hook(
|
|
@@ -2254,6 +2286,7 @@ class DataServiceMixin:
|
|
|
2254
2286
|
if not table:
|
|
2255
2287
|
raise ValueError("Parameter 'obj' cannot be empty")
|
|
2256
2288
|
self._assert_client_table(table, "obj")
|
|
2289
|
+
self._audit_confidential_read(tx, table, context)
|
|
2257
2290
|
payload["params"] = self._client_query_params(tx, table, payload.get("params"))
|
|
2258
2291
|
|
|
2259
2292
|
result_format = payload.get("result_format")
|
|
@@ -2438,6 +2471,7 @@ class DataServiceMixin:
|
|
|
2438
2471
|
|
|
2439
2472
|
self._assert_direct_access_allowed(table_name)
|
|
2440
2473
|
self._assert_direct_unscoped(table_name, context)
|
|
2474
|
+
self._audit_confidential_read(tx, table_name, context)
|
|
2441
2475
|
|
|
2442
2476
|
params = self._client_query_params(tx, table_name, payload.get("params", {}))
|
|
2443
2477
|
|
{velocity_python-0.2.8 → velocity_python-0.2.9}/src/velocity/aws/handlers/mixins/tiles_actions.py
RENAMED
|
@@ -112,6 +112,10 @@ class TilesMixin:
|
|
|
112
112
|
rule with the viewer filled in, and that where."""
|
|
113
113
|
table = tile["target"]
|
|
114
114
|
self._assert_client_table(table, "target")
|
|
115
|
+
# A confidential table (the security audit) is never a tile target:
|
|
116
|
+
# a saved, re-counted filter over it would be an unaudited oracle.
|
|
117
|
+
if authz.is_confidential(table):
|
|
118
|
+
raise AlertError(f"'{table}' can't be counted on a tile.")
|
|
115
119
|
rule = tiles.fill_rule(
|
|
116
120
|
tiles.clean_rule(_as_dict(tile.get("rule"))),
|
|
117
121
|
principal,
|
|
@@ -218,7 +222,10 @@ class TilesMixin:
|
|
|
218
222
|
return declared is not None and (
|
|
219
223
|
not declared.permission or principal.holds(declared.permission)
|
|
220
224
|
)
|
|
221
|
-
|
|
225
|
+
target = str(tile.get("target") or "")
|
|
226
|
+
if authz.is_confidential(target):
|
|
227
|
+
return False
|
|
228
|
+
return bool(self._client_table_allowed(target))
|
|
222
229
|
|
|
223
230
|
def _tile_public(self, tx, context, tile, principal, today, fresh=False, version=0):
|
|
224
231
|
"""One tile as the browser gets it, or None when it is not this
|
|
@@ -21,7 +21,8 @@ renames or retypes an existing column.
|
|
|
21
21
|
|
|
22
22
|
Groups:
|
|
23
23
|
|
|
24
|
-
* ``core`` — the tables every velocity handler app writes
|
|
24
|
+
* ``core`` — the tables every velocity handler app writes, including the
|
|
25
|
+
``sys_security_events`` audit sink (SEC-12).
|
|
25
26
|
* ``assets`` — ``images`` + ``asset_usage_index`` (:mod:`velocity.aws.assets`).
|
|
26
27
|
* ``payment`` — processor mirror tables (:mod:`velocity.payment.schema`).
|
|
27
28
|
* ``permissions`` — roles, grants and requests (:mod:`velocity.aws.permissions`).
|
|
@@ -50,6 +51,7 @@ SCHEMA_SOURCES: Dict[str, Tuple[str, ...]] = {
|
|
|
50
51
|
"velocity.aws.handlers.mixins.web_handler",
|
|
51
52
|
"velocity.aws.handlers.mixins.session_control",
|
|
52
53
|
"velocity.aws.user_data",
|
|
54
|
+
"velocity.aws.capabilities.security",
|
|
53
55
|
"velocity.db.migrations",
|
|
54
56
|
),
|
|
55
57
|
"assets": ("velocity.aws.assets",),
|
|
@@ -377,6 +377,7 @@ tests/test_schema_lock_config_other_backends.py
|
|
|
377
377
|
tests/test_schema_migrations.py
|
|
378
378
|
tests/test_search_paging_export.py
|
|
379
379
|
tests/test_security_capability.py
|
|
380
|
+
tests/test_security_events_table_gate.py
|
|
380
381
|
tests/test_security_hardening.py
|
|
381
382
|
tests/test_server_cursor.py
|
|
382
383
|
tests/test_session_control.py
|
|
@@ -41,10 +41,17 @@ class FakeTable:
|
|
|
41
41
|
return 3
|
|
42
42
|
|
|
43
43
|
exists_flag = True
|
|
44
|
+
# sys_columns() of the live table; None = every declared events column
|
|
45
|
+
columns = None
|
|
44
46
|
|
|
45
47
|
def exists(self):
|
|
46
48
|
return self.exists_flag
|
|
47
49
|
|
|
50
|
+
def sys_columns(self):
|
|
51
|
+
if self.columns is not None:
|
|
52
|
+
return list(self.columns)
|
|
53
|
+
return ["sys_id", "sys_created", *sec.EVENT_COLUMNS]
|
|
54
|
+
|
|
48
55
|
|
|
49
56
|
class FakeTx:
|
|
50
57
|
def __init__(self):
|
|
@@ -305,3 +312,73 @@ def test_capability_declares_schema_switches_and_prunes_with_purge():
|
|
|
305
312
|
assert "<sys_created" in tx.table(sec.EVENTS_TABLE).purges[0]
|
|
306
313
|
with pytest.raises(ValueError):
|
|
307
314
|
cap.handle("nope", tx, None, {})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# -- sys_security_events as velocity's core audit sink (SEC-12, Paul 2026-10-25)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_record_event_stores_subject_and_detail():
|
|
321
|
+
tx = FakeTx()
|
|
322
|
+
ok = sec.record_event(
|
|
323
|
+
tx,
|
|
324
|
+
"auth.login",
|
|
325
|
+
"signed in",
|
|
326
|
+
user_email="staff@example.com",
|
|
327
|
+
user_sub="sub-123",
|
|
328
|
+
detail={"client_id": "abc", "new_device": True},
|
|
329
|
+
)
|
|
330
|
+
assert ok is True and tx.commits == 1
|
|
331
|
+
row = tx.table(sec.EVENTS_TABLE).rows[0]
|
|
332
|
+
assert row["guard"] == "auth.login" and row["user_sub"] == "sub-123"
|
|
333
|
+
assert row["detail"] == {"client_id": "abc", "new_device": True}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_record_event_on_a_table_older_than_the_new_columns_still_records():
|
|
337
|
+
# a site whose schema lane has not added user_sub/detail yet: on a locked
|
|
338
|
+
# schema the unknown columns would lose the whole event, so they are left out
|
|
339
|
+
tx = FakeTx()
|
|
340
|
+
table = tx.table(sec.EVENTS_TABLE)
|
|
341
|
+
table.columns = ["sys_id", "guard", "reason", "ip", "user_email"]
|
|
342
|
+
assert sec.record_event(tx, "masquerade.start", "x", user_sub="s", detail={"a": 1})
|
|
343
|
+
row = table.rows[0]
|
|
344
|
+
assert row["guard"] == "masquerade.start"
|
|
345
|
+
assert "user_sub" not in row and "detail" not in row
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_every_event_says_when_it_occurred():
|
|
349
|
+
tx = FakeTx()
|
|
350
|
+
assert sec.record_event(tx, "ip_denylist", "listed")
|
|
351
|
+
row = tx.table(sec.EVENTS_TABLE).rows[0]
|
|
352
|
+
assert row["occurred_at"].tzinfo is not None
|
|
353
|
+
when = datetime.datetime(2026, 10, 25, 15, tzinfo=datetime.timezone.utc)
|
|
354
|
+
assert sec.record_event(tx, "auth.login", "signed in", occurred_at=when)
|
|
355
|
+
assert tx.table(sec.EVENTS_TABLE).rows[1]["occurred_at"] == when
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_events_table_is_core_and_retention_defaults_to_a_year():
|
|
359
|
+
from velocity.aws import schema as infra
|
|
360
|
+
|
|
361
|
+
core = infra.infrastructure_schema()
|
|
362
|
+
assert core[sec.EVENTS_TABLE]["columns"]["detail"] == "JSONB"
|
|
363
|
+
assert core[sec.EVENTS_TABLE]["columns"]["user_sub"] == "TEXT"
|
|
364
|
+
assert sec.KNOBS["security_events_days"] == "365"
|
|
365
|
+
# the capability still declares it, so enabling it on an older site ensures it
|
|
366
|
+
assert sec.Security_instance().schema[sec.EVENTS_TABLE] == sec.EVENT_COLUMNS
|
|
367
|
+
tx = FakeTx()
|
|
368
|
+
sec.Security_instance().handle("prune", tx, None, {})
|
|
369
|
+
cutoff = tx.table(sec.EVENTS_TABLE).purges[0]["<sys_created"]
|
|
370
|
+
age = datetime.datetime.now(datetime.timezone.utc) - cutoff
|
|
371
|
+
assert 364 < age.days <= 365
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_an_event_still_lands_when_the_columns_cannot_be_read():
|
|
375
|
+
tx = FakeTx()
|
|
376
|
+
table = tx.table(sec.EVENTS_TABLE)
|
|
377
|
+
|
|
378
|
+
def broken():
|
|
379
|
+
raise RuntimeError("catalog unavailable")
|
|
380
|
+
|
|
381
|
+
table.sys_columns = broken
|
|
382
|
+
assert sec.record_event(tx, "masquerade", "acting as x", user_sub="s")
|
|
383
|
+
row = table.rows[0]
|
|
384
|
+
assert row["guard"] == "masquerade" and "user_sub" not in row
|