velocity-python 0.1.106__tar.gz → 0.1.107__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.106 → velocity_python-0.1.107}/PKG-INFO +1 -1
- {velocity_python-0.1.106 → velocity_python-0.1.107}/pyproject.toml +1 -1
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/__init__.py +18 -1
- velocity_python-0.1.107/src/velocity/aws/capabilities/base.py +254 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/dispatch.py +12 -1
- velocity_python-0.1.107/src/velocity/aws/capabilities/infra.py +121 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/jobs.py +6 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/retention.py +18 -1
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/security.py +72 -4
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/single_session.py +7 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- velocity_python-0.1.107/tests/test_capability_catalog.py +188 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_security_capability.py +15 -1
- velocity_python-0.1.106/src/velocity/aws/capabilities/base.py +0 -79
- velocity_python-0.1.106/src/velocity/aws/capabilities/infra.py +0 -43
- {velocity_python-0.1.106 → velocity_python-0.1.107}/LICENSE +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/README.md +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/setup.cfg +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/provisioning.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/feature_flags.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/job_lifecycle.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_access_error_403.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_blob_store.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_capabilities.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_context_flags_loader.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_feature_flags.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_job_lifecycle.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_observability.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_response.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_session_control.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_single_session_capability.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_table_purge.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.106 → velocity_python-0.1.107}/tests/test_write_hook_create_flow.py +0 -0
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/__init__.py
RENAMED
|
@@ -10,13 +10,23 @@ from .base import ( # noqa: F401
|
|
|
10
10
|
EVERY_15_MINUTES,
|
|
11
11
|
HOURLY,
|
|
12
12
|
NIGHTLY,
|
|
13
|
+
POLICY_KINDS,
|
|
13
14
|
REGISTRY,
|
|
14
15
|
Capability,
|
|
15
16
|
capability,
|
|
17
|
+
catalog,
|
|
18
|
+
describe,
|
|
16
19
|
get,
|
|
20
|
+
load_config,
|
|
21
|
+
policy_field,
|
|
17
22
|
resolve_config,
|
|
23
|
+
validate_policy,
|
|
24
|
+
)
|
|
25
|
+
from .dispatch import ( # noqa: F401
|
|
26
|
+
CapabilityCatalogMixin,
|
|
27
|
+
CapabilityDispatcher,
|
|
28
|
+
run_capability,
|
|
18
29
|
)
|
|
19
|
-
from .dispatch import CapabilityDispatcher, run_capability # noqa: F401
|
|
20
30
|
from .provisioning import provision, trigger_key # noqa: F401
|
|
21
31
|
|
|
22
32
|
# built-ins (importing registers them)
|
|
@@ -29,14 +39,21 @@ from .security import SecurityMixin # noqa: E402,F401
|
|
|
29
39
|
|
|
30
40
|
__all__ = [
|
|
31
41
|
"Capability",
|
|
42
|
+
"CapabilityCatalogMixin",
|
|
32
43
|
"CapabilityDispatcher",
|
|
44
|
+
"POLICY_KINDS",
|
|
33
45
|
"REGISTRY",
|
|
34
46
|
"capability",
|
|
47
|
+
"catalog",
|
|
48
|
+
"describe",
|
|
35
49
|
"get",
|
|
50
|
+
"load_config",
|
|
51
|
+
"policy_field",
|
|
36
52
|
"provision",
|
|
37
53
|
"resolve_config",
|
|
38
54
|
"run_capability",
|
|
39
55
|
"trigger_key",
|
|
56
|
+
"validate_policy",
|
|
40
57
|
"NIGHTLY",
|
|
41
58
|
"HOURLY",
|
|
42
59
|
"EVERY_15_MINUTES",
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Capabilities: self-describing system-level features a site can turn on.
|
|
2
|
+
|
|
3
|
+
A whole class of features has the same shape — some schema, a recurring
|
|
4
|
+
schedule, a handler, a few config knobs, maybe a handler mixin: retention
|
|
5
|
+
pruning, session reaping, the feature-flag mirror, mail harvest, demo-safety
|
|
6
|
+
scrubs, ops metrics. A :class:`Capability` declares all of that in ONE object,
|
|
7
|
+
so the deploy pipeline can materialise it (see :mod:`provisioning`) and the
|
|
8
|
+
queue handler can run it (see :mod:`dispatch`) without a bespoke migration or
|
|
9
|
+
wiring per feature.
|
|
10
|
+
|
|
11
|
+
Division of labour:
|
|
12
|
+
|
|
13
|
+
* **code** (a subclass registered with :func:`capability`) says what the
|
|
14
|
+
feature *is* — its schema, default cadence, default knobs, and the work;
|
|
15
|
+
* the **control plane** (the site registry) says which sites/environments run
|
|
16
|
+
it and with what policy, delivered to the site as an entry that
|
|
17
|
+
:func:`provisioning.provision` materialises.
|
|
18
|
+
|
|
19
|
+
Config resolution (:func:`resolve_config`): the capability's ``defaults`` are
|
|
20
|
+
the floor; the environment's feature-flag row overrides the knobs the
|
|
21
|
+
capability declares (so they are edited on the Feature Flags page and mirrored
|
|
22
|
+
between databases); the site's registry ``config`` overrides everything (the
|
|
23
|
+
per-site policy — e.g. *which* tables retention may touch).
|
|
24
|
+
|
|
25
|
+
A capability also **describes itself** — ``label``, ``description`` and a
|
|
26
|
+
``policy`` schema (:func:`policy_field`) — so an admin UI can offer the
|
|
27
|
+
registry as a pick-list and render a real editor for each policy instead of
|
|
28
|
+
a JSON box (:func:`catalog`), and so a write can be checked against the same
|
|
29
|
+
declaration before it lands (:func:`validate_policy`). One declaration, next
|
|
30
|
+
to the code that reads the config; nothing to keep in step by hand.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import ipaddress
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
|
|
39
|
+
# cron expressions the scheduler (croniter) accepts
|
|
40
|
+
NIGHTLY = "0 3 * * *"
|
|
41
|
+
HOURLY = "0 * * * *"
|
|
42
|
+
EVERY_15_MINUTES = "*/15 * * * *"
|
|
43
|
+
CRON_WORDS = {
|
|
44
|
+
NIGHTLY: "nightly at 03:00 UTC",
|
|
45
|
+
HOURLY: "hourly",
|
|
46
|
+
EVERY_15_MINUTES: "every 15 minutes",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# policy field kinds an editor knows how to render and this module validates
|
|
50
|
+
CIDR_LIST = "cidr_list" # ["203.0.113.0/24", "198.51.100.7"]
|
|
51
|
+
COUNTRY_LIST = "country_list" # ["US", "CA"] ISO 3166-1 alpha-2
|
|
52
|
+
TABLE_DAYS = "table_days" # {"aws_api_activity": 90}
|
|
53
|
+
NUMBER = "number"
|
|
54
|
+
TEXT = "text"
|
|
55
|
+
BOOLEAN = "boolean"
|
|
56
|
+
CHOICE = "choice" # one of `options`
|
|
57
|
+
LIST = "list" # free strings
|
|
58
|
+
POLICY_KINDS = (CIDR_LIST, COUNTRY_LIST, TABLE_DAYS, NUMBER, TEXT, BOOLEAN, CHOICE, LIST)
|
|
59
|
+
|
|
60
|
+
IDENTIFIER = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
61
|
+
COUNTRY_CODE = re.compile(r"^[A-Z]{2}$")
|
|
62
|
+
|
|
63
|
+
REGISTRY: dict = {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def policy_field(name, kind, label, help="", **extra) -> dict:
|
|
67
|
+
"""One entry of a capability's ``policy`` schema. ``extra`` carries
|
|
68
|
+
kind-specific hints an editor may use (``options`` for a choice,
|
|
69
|
+
``default``, ``placeholder``, ``multiline``, ``min``)."""
|
|
70
|
+
if kind not in POLICY_KINDS:
|
|
71
|
+
raise ValueError(f"policy field {name!r}: unknown kind {kind!r}")
|
|
72
|
+
return {"name": name, "kind": kind, "label": label, "help": help, **extra}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Capability:
|
|
76
|
+
name: str = ""
|
|
77
|
+
label: str = "" # human name; defaults to the name titled
|
|
78
|
+
description: str = "" # one or two sentences for the pick-list
|
|
79
|
+
tier: str = "runtime" # "runtime" (a Lambda runs it) | "infra" (stacks)
|
|
80
|
+
schema: dict = {} # {table: {column: sql type}} -> ensure_schema
|
|
81
|
+
defaults: dict = {} # flag name -> default; seeded as TEXT feature flags
|
|
82
|
+
schedules: tuple = () # ((action, cron), ...)
|
|
83
|
+
mixins: tuple = () # handler mixins the site attaches at runtime
|
|
84
|
+
applies_to: tuple = ("demo", "production") # default stages; registry can force
|
|
85
|
+
policy: tuple = () # (policy_field(...), ...) — the per-site config shape
|
|
86
|
+
|
|
87
|
+
def handle(self, action, tx, context, config):
|
|
88
|
+
"""Do the work for one scheduled/queued ``action``. Return a JSON-able
|
|
89
|
+
summary (it lands in the queue response and the job log)."""
|
|
90
|
+
raise NotImplementedError(f"{self.name}: no handler for action {action!r}")
|
|
91
|
+
|
|
92
|
+
def on_refresh(self, tx, config):
|
|
93
|
+
"""Hook run after a demo refresh (optional) — scrub/neutralise here."""
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def capability(cls):
|
|
98
|
+
"""Class decorator: instantiate and register under ``cls.name``."""
|
|
99
|
+
if not getattr(cls, "name", ""):
|
|
100
|
+
raise ValueError(f"{cls.__name__} must set a non-empty `name`")
|
|
101
|
+
REGISTRY[cls.name] = cls()
|
|
102
|
+
return cls
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get(name):
|
|
106
|
+
try:
|
|
107
|
+
return REGISTRY[name]
|
|
108
|
+
except KeyError:
|
|
109
|
+
known = ", ".join(sorted(REGISTRY)) or "(none registered)"
|
|
110
|
+
raise KeyError(f"unknown capability {name!r}; registered: {known}") from None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_config(cap, flags=None, row_config=None) -> dict:
|
|
114
|
+
"""defaults <- feature flags (declared knobs only) <- site registry config."""
|
|
115
|
+
config = dict(cap.defaults)
|
|
116
|
+
flags = flags or {}
|
|
117
|
+
for key in cap.defaults:
|
|
118
|
+
value = flags.get(key)
|
|
119
|
+
if value not in (None, ""):
|
|
120
|
+
config[key] = value
|
|
121
|
+
config.update(row_config or {})
|
|
122
|
+
return config
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── self-description ────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def describe(cap) -> dict:
|
|
129
|
+
"""JSON-able description of one capability for a UI or a CLI."""
|
|
130
|
+
return {
|
|
131
|
+
"name": cap.name,
|
|
132
|
+
"label": cap.label or cap.name.replace("_", " ").title(),
|
|
133
|
+
"description": cap.description,
|
|
134
|
+
"tier": cap.tier,
|
|
135
|
+
"applies_to": list(cap.applies_to),
|
|
136
|
+
"schedules": [
|
|
137
|
+
{"action": action, "cron": cron, "when": CRON_WORDS.get(cron, cron)}
|
|
138
|
+
for action, cron in cap.schedules
|
|
139
|
+
],
|
|
140
|
+
"flags": list(cap.defaults),
|
|
141
|
+
"policy": [dict(field) for field in cap.policy],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def catalog() -> list:
|
|
146
|
+
"""Every registered capability, described, ordered by label — the
|
|
147
|
+
pick-list an admin UI shows."""
|
|
148
|
+
return sorted((describe(cap) for cap in REGISTRY.values()), key=lambda d: d["label"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_config(raw) -> dict:
|
|
152
|
+
"""A row's ``config`` as a dict: JSON text, a dict, or empty."""
|
|
153
|
+
if raw in (None, ""):
|
|
154
|
+
return {}
|
|
155
|
+
if isinstance(raw, str):
|
|
156
|
+
value = json.loads(raw)
|
|
157
|
+
else:
|
|
158
|
+
value = raw
|
|
159
|
+
if not isinstance(value, dict):
|
|
160
|
+
raise ValueError("policy must be a JSON object")
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def validate_policy(name, config) -> list:
|
|
165
|
+
"""Check a site's ``config`` against the capability's declared policy.
|
|
166
|
+
|
|
167
|
+
Returns human-readable problems (empty = valid). Declared fields are
|
|
168
|
+
typed; undeclared keys pass through untouched — a policy may carry more
|
|
169
|
+
than the editor knows (an older row, a hand edit), and the capability
|
|
170
|
+
decides what it reads. An unknown capability name is itself a problem.
|
|
171
|
+
"""
|
|
172
|
+
cap = REGISTRY.get(name)
|
|
173
|
+
if cap is None:
|
|
174
|
+
known = ", ".join(sorted(REGISTRY)) or "(none registered)"
|
|
175
|
+
return [f"unknown capability {name!r}; registered: {known}"]
|
|
176
|
+
try:
|
|
177
|
+
config = load_config(config)
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
return [f"policy is not valid JSON: {exc}"]
|
|
180
|
+
|
|
181
|
+
problems = []
|
|
182
|
+
for field in cap.policy:
|
|
183
|
+
value = config.get(field["name"])
|
|
184
|
+
if value in (None, "", [], {}):
|
|
185
|
+
continue
|
|
186
|
+
problems.extend(
|
|
187
|
+
f"{field['label']}: {problem}"
|
|
188
|
+
for problem in _check_field(field, value)
|
|
189
|
+
)
|
|
190
|
+
return problems
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _check_field(field, value):
|
|
194
|
+
kind = field["kind"]
|
|
195
|
+
if kind in (CIDR_LIST, COUNTRY_LIST, LIST):
|
|
196
|
+
if not isinstance(value, (list, tuple)):
|
|
197
|
+
return ["must be a list"]
|
|
198
|
+
problems = []
|
|
199
|
+
for item in value:
|
|
200
|
+
item = str(item).strip()
|
|
201
|
+
if kind == CIDR_LIST:
|
|
202
|
+
try:
|
|
203
|
+
ipaddress.ip_network(item, strict=False)
|
|
204
|
+
except ValueError:
|
|
205
|
+
problems.append(f"{item!r} is not an IP address or CIDR")
|
|
206
|
+
elif kind == COUNTRY_LIST and not COUNTRY_CODE.match(item.upper()):
|
|
207
|
+
problems.append(f"{item!r} is not a two-letter country code")
|
|
208
|
+
return problems
|
|
209
|
+
if kind == TABLE_DAYS:
|
|
210
|
+
if not isinstance(value, dict):
|
|
211
|
+
return ["must map table names to days"]
|
|
212
|
+
problems = []
|
|
213
|
+
for table, days in value.items():
|
|
214
|
+
if not IDENTIFIER.match(str(table)):
|
|
215
|
+
problems.append(f"{table!r} is not a table name")
|
|
216
|
+
if days in (None, "", 0):
|
|
217
|
+
continue # falls back to the default window
|
|
218
|
+
if not _is_int(days) or int(days) < 1:
|
|
219
|
+
problems.append(f"{table}: days must be a whole number >= 1")
|
|
220
|
+
return problems
|
|
221
|
+
if kind == NUMBER:
|
|
222
|
+
if isinstance(value, bool) or not _is_number(value):
|
|
223
|
+
return ["must be a number"]
|
|
224
|
+
minimum = field.get("min")
|
|
225
|
+
if minimum is not None and float(value) < minimum:
|
|
226
|
+
return [f"must be >= {minimum}"]
|
|
227
|
+
return []
|
|
228
|
+
if kind == BOOLEAN:
|
|
229
|
+
ok = isinstance(value, bool) or str(value).lower() in ("true", "false")
|
|
230
|
+
return [] if ok else ["must be true or false"]
|
|
231
|
+
if kind == CHOICE:
|
|
232
|
+
options = field.get("options") or ()
|
|
233
|
+
return [] if value in options else [f"must be one of {', '.join(options)}"]
|
|
234
|
+
if kind == TEXT:
|
|
235
|
+
return [] if isinstance(value, str) else ["must be text"]
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _is_int(value) -> bool:
|
|
240
|
+
if isinstance(value, bool):
|
|
241
|
+
return False
|
|
242
|
+
if isinstance(value, int):
|
|
243
|
+
return True
|
|
244
|
+
return isinstance(value, str) and value.strip().isdigit()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _is_number(value) -> bool:
|
|
248
|
+
if isinstance(value, (int, float)):
|
|
249
|
+
return True
|
|
250
|
+
try:
|
|
251
|
+
float(str(value))
|
|
252
|
+
return True
|
|
253
|
+
except ValueError:
|
|
254
|
+
return False
|
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/dispatch.py
RENAMED
|
@@ -13,7 +13,7 @@ import json
|
|
|
13
13
|
|
|
14
14
|
from velocity.aws import feature_flags
|
|
15
15
|
|
|
16
|
-
from .base import REGISTRY, resolve_config
|
|
16
|
+
from .base import REGISTRY, catalog, resolve_config
|
|
17
17
|
from .provisioning import SITE_TABLE
|
|
18
18
|
|
|
19
19
|
|
|
@@ -48,3 +48,14 @@ class CapabilityDispatcher:
|
|
|
48
48
|
tx, context, payload.get("capability"), payload.get("action")
|
|
49
49
|
)
|
|
50
50
|
context.response().set_body(body)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CapabilityCatalogMixin:
|
|
54
|
+
"""Web-handler mixin for the control plane: ``capability-catalog`` answers
|
|
55
|
+
with every registered capability described (``{"rows": [...]}`` — the
|
|
56
|
+
shape velocity-react's ApiSelect and option hooks consume), so the admin
|
|
57
|
+
UI's pick-list and policy editors come from the code, not a copy."""
|
|
58
|
+
|
|
59
|
+
def OnActionCapabilityCatalog(self, tx, context):
|
|
60
|
+
del tx
|
|
61
|
+
context.response().set_body({"rows": catalog()})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Infra-tier capabilities: rendered into a site's CloudFormation stacks by
|
|
2
|
+
the sitefactory templates rather than run by a Lambda.
|
|
3
|
+
|
|
4
|
+
They are registered here so the registry recognises their names (the
|
|
5
|
+
provisioner would otherwise refuse an unknown capability) and so they sit
|
|
6
|
+
on the same Site Capabilities page as everything else; their ``config`` is
|
|
7
|
+
the template's policy. Nothing here executes at runtime.
|
|
8
|
+
|
|
9
|
+
* ``security_headers`` — the CloudFront response-headers policy on the web
|
|
10
|
+
distribution (HSTS, nosniff, frame options, referrer policy, XSS-off, and
|
|
11
|
+
an optional ``csp``). Free; ON by default for every factory site — a row
|
|
12
|
+
is only needed to turn it OFF or to set a Content-Security-Policy.
|
|
13
|
+
* ``api_throttling`` — API Gateway stage throttling (``rate`` requests/s,
|
|
14
|
+
``burst``). Free; the template defaults apply without a row.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .base import BOOLEAN, CHOICE, NUMBER, TEXT, Capability, capability, policy_field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InfraCapability(Capability):
|
|
23
|
+
tier = "infra"
|
|
24
|
+
schedules = ()
|
|
25
|
+
schema = {}
|
|
26
|
+
|
|
27
|
+
def handle(self, action, tx, context, config):
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
f"{self.name} is an infra capability rendered into the site's stacks "
|
|
30
|
+
f"by sitefactory; it has no runtime action ({action!r})"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@capability
|
|
35
|
+
class SecurityHeaders(InfraCapability):
|
|
36
|
+
name = "security_headers"
|
|
37
|
+
label = "Security headers (CloudFront)"
|
|
38
|
+
description = (
|
|
39
|
+
"Browser hardening headers on every web response: HSTS, nosniff, frame "
|
|
40
|
+
"options, referrer policy, and an optional Content-Security-Policy. "
|
|
41
|
+
"Free; on for every factory site by default — add a row to turn it "
|
|
42
|
+
"off or to set a CSP. Applied by site-update-web, not the backend deploy."
|
|
43
|
+
)
|
|
44
|
+
# the keys sitefactory.templates.web.security_headers_config reads
|
|
45
|
+
policy = (
|
|
46
|
+
policy_field(
|
|
47
|
+
"enabled",
|
|
48
|
+
BOOLEAN,
|
|
49
|
+
"Send security headers",
|
|
50
|
+
"Off removes the headers policy from the distribution.",
|
|
51
|
+
default=True,
|
|
52
|
+
),
|
|
53
|
+
policy_field(
|
|
54
|
+
"csp",
|
|
55
|
+
TEXT,
|
|
56
|
+
"Content-Security-Policy",
|
|
57
|
+
"Optional. Sent verbatim; a wrong CSP blocks your own scripts, so "
|
|
58
|
+
"test on demo first.",
|
|
59
|
+
multiline=True,
|
|
60
|
+
placeholder="default-src 'self'; img-src 'self' data:",
|
|
61
|
+
),
|
|
62
|
+
policy_field(
|
|
63
|
+
"frame_options",
|
|
64
|
+
CHOICE,
|
|
65
|
+
"Frame options",
|
|
66
|
+
"SAMEORIGIN lets the site frame itself; DENY forbids all framing.",
|
|
67
|
+
options=("SAMEORIGIN", "DENY"),
|
|
68
|
+
default="SAMEORIGIN",
|
|
69
|
+
),
|
|
70
|
+
policy_field(
|
|
71
|
+
"referrer_policy",
|
|
72
|
+
CHOICE,
|
|
73
|
+
"Referrer policy",
|
|
74
|
+
"",
|
|
75
|
+
options=(
|
|
76
|
+
"strict-origin-when-cross-origin",
|
|
77
|
+
"no-referrer",
|
|
78
|
+
"same-origin",
|
|
79
|
+
"strict-origin",
|
|
80
|
+
),
|
|
81
|
+
default="strict-origin-when-cross-origin",
|
|
82
|
+
),
|
|
83
|
+
policy_field(
|
|
84
|
+
"hsts_max_age",
|
|
85
|
+
NUMBER,
|
|
86
|
+
"HSTS max-age (seconds)",
|
|
87
|
+
"Two years (63072000) is the preload-list minimum.",
|
|
88
|
+
default=63072000,
|
|
89
|
+
min=0,
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@capability
|
|
95
|
+
class ApiThrottling(InfraCapability):
|
|
96
|
+
name = "api_throttling"
|
|
97
|
+
label = "API throttling (API Gateway)"
|
|
98
|
+
description = (
|
|
99
|
+
"Stage-level request throttling on the private and public APIs — the "
|
|
100
|
+
"first, free line against floods. Free; the template defaults (100 "
|
|
101
|
+
"requests/s, burst 200) apply without a row. Applied by site-up."
|
|
102
|
+
)
|
|
103
|
+
# the keys sitefactory.driver reads for the api template
|
|
104
|
+
policy = (
|
|
105
|
+
policy_field(
|
|
106
|
+
"rate",
|
|
107
|
+
NUMBER,
|
|
108
|
+
"Steady rate (requests / second)",
|
|
109
|
+
"",
|
|
110
|
+
default=100,
|
|
111
|
+
min=1,
|
|
112
|
+
),
|
|
113
|
+
policy_field(
|
|
114
|
+
"burst",
|
|
115
|
+
NUMBER,
|
|
116
|
+
"Burst (requests)",
|
|
117
|
+
"Short spikes above the steady rate that are still served.",
|
|
118
|
+
default=200,
|
|
119
|
+
min=1,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
@@ -26,6 +26,12 @@ from .base import EVERY_15_MINUTES, Capability, capability
|
|
|
26
26
|
@capability
|
|
27
27
|
class Jobs(Capability):
|
|
28
28
|
name = "jobs"
|
|
29
|
+
label = "Job hygiene"
|
|
30
|
+
description = (
|
|
31
|
+
"Marks asynchronous jobs whose worker died as Failed (instead of "
|
|
32
|
+
"Running forever) and prunes old job activity. Windows are per-"
|
|
33
|
+
"environment flags; no per-site policy."
|
|
34
|
+
)
|
|
29
35
|
defaults = {"job_stale_minutes": "16", "job_retention_days": "30"}
|
|
30
36
|
schedules = (("expire_stale", EVERY_15_MINUTES),)
|
|
31
37
|
|
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/retention.py
RENAMED
|
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
import datetime
|
|
19
19
|
import re
|
|
20
20
|
|
|
21
|
-
from .base import NIGHTLY, Capability, capability
|
|
21
|
+
from .base import NIGHTLY, TABLE_DAYS, Capability, capability, policy_field
|
|
22
22
|
|
|
23
23
|
IDENTIFIER = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
24
24
|
TIMESTAMP_COLUMN = "sys_created"
|
|
@@ -29,8 +29,25 @@ MAX_BATCHES = 200 # 1M rows per run — well inside the Lambda budget
|
|
|
29
29
|
@capability
|
|
30
30
|
class Retention(Capability):
|
|
31
31
|
name = "retention"
|
|
32
|
+
label = "Retention (table pruning)"
|
|
33
|
+
description = (
|
|
34
|
+
"Deletes rows older than a per-table window so log-like tables stop "
|
|
35
|
+
"growing forever. Only the tables listed in the policy are ever touched; "
|
|
36
|
+
"rows are aged by sys_created and deleted nightly in small batches."
|
|
37
|
+
)
|
|
32
38
|
defaults = {"purge_default_days": "90"}
|
|
33
39
|
schedules = (("purge", NIGHTLY),)
|
|
40
|
+
policy = (
|
|
41
|
+
policy_field(
|
|
42
|
+
"tables",
|
|
43
|
+
TABLE_DAYS,
|
|
44
|
+
"Tables to prune",
|
|
45
|
+
"Each table and how many days of rows to keep. Leave days blank to "
|
|
46
|
+
"use the purge_default_days flag. Archives are not logs — list only "
|
|
47
|
+
"tables you are happy to lose old rows from.",
|
|
48
|
+
placeholder="aws_api_activity",
|
|
49
|
+
),
|
|
50
|
+
)
|
|
34
51
|
|
|
35
52
|
def handle(self, action, tx, context, config):
|
|
36
53
|
if action != "purge":
|
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/security.py
RENAMED
|
@@ -31,7 +31,15 @@ import time
|
|
|
31
31
|
|
|
32
32
|
from velocity.aws import feature_flags
|
|
33
33
|
|
|
34
|
-
from .base import
|
|
34
|
+
from .base import (
|
|
35
|
+
CIDR_LIST,
|
|
36
|
+
COUNTRY_LIST,
|
|
37
|
+
HOURLY,
|
|
38
|
+
Capability,
|
|
39
|
+
capability,
|
|
40
|
+
policy_field,
|
|
41
|
+
resolve_config,
|
|
42
|
+
)
|
|
35
43
|
from .provisioning import SITE_TABLE
|
|
36
44
|
|
|
37
45
|
log = logging.getLogger(__name__)
|
|
@@ -174,9 +182,22 @@ def _cached(key, loader, *, now=None):
|
|
|
174
182
|
|
|
175
183
|
|
|
176
184
|
def _site_row(tx):
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
"""The site's `security` row from sys_capabilities, cached per container.
|
|
186
|
+
|
|
187
|
+
A site that has never provisioned a capability has no sys_capabilities
|
|
188
|
+
table at all — a bare find() there is a failing statement on every request
|
|
189
|
+
(rolled back and logged by velocity, but noise and a wasted round trip).
|
|
190
|
+
exists() is a cheap catalog check and the honest answer: not provisioned
|
|
191
|
+
here, so the mixin is a no-op.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def load():
|
|
195
|
+
table = tx.table(SITE_TABLE)
|
|
196
|
+
if not table.exists():
|
|
197
|
+
return None
|
|
198
|
+
return table.find({"name": "security"})
|
|
199
|
+
|
|
200
|
+
return _cached(("site", "security"), load)
|
|
180
201
|
|
|
181
202
|
|
|
182
203
|
def _flags(tx):
|
|
@@ -311,7 +332,54 @@ class SecurityMixin:
|
|
|
311
332
|
@capability
|
|
312
333
|
class Security(Capability):
|
|
313
334
|
name = "security"
|
|
335
|
+
label = "Security guards"
|
|
336
|
+
description = (
|
|
337
|
+
"Request-path guards in the application: IP deny/allow lists, country "
|
|
338
|
+
"allow/block, per-IP and per-user rate limits, payload caps, and an "
|
|
339
|
+
"audit of every refusal. Each guard is a switch on the Security page; "
|
|
340
|
+
"this policy holds the lists."
|
|
341
|
+
)
|
|
314
342
|
defaults = {**SWITCHES, **KNOBS}
|
|
343
|
+
policy = (
|
|
344
|
+
policy_field(
|
|
345
|
+
"ip_denylist",
|
|
346
|
+
CIDR_LIST,
|
|
347
|
+
"IP deny list",
|
|
348
|
+
"Addresses or CIDR ranges refused outright (when the IP deny list "
|
|
349
|
+
"switch is on).",
|
|
350
|
+
placeholder="203.0.113.0/24",
|
|
351
|
+
),
|
|
352
|
+
policy_field(
|
|
353
|
+
"ip_allowlist",
|
|
354
|
+
CIDR_LIST,
|
|
355
|
+
"IP allow list",
|
|
356
|
+
"When the IP allow list switch is on, ONLY these addresses may call "
|
|
357
|
+
"— an office/VPN lock-down. Leave empty for no restriction.",
|
|
358
|
+
placeholder="198.51.100.0/24",
|
|
359
|
+
),
|
|
360
|
+
policy_field(
|
|
361
|
+
"blocked_countries",
|
|
362
|
+
COUNTRY_LIST,
|
|
363
|
+
"Blocked countries",
|
|
364
|
+
"Requests from these countries are refused (country switch on; "
|
|
365
|
+
"judged from the CloudFront viewer country).",
|
|
366
|
+
),
|
|
367
|
+
policy_field(
|
|
368
|
+
"allowed_countries",
|
|
369
|
+
COUNTRY_LIST,
|
|
370
|
+
"Allowed countries",
|
|
371
|
+
"When set, only these countries may call. Leave empty to allow all "
|
|
372
|
+
"but the blocked ones.",
|
|
373
|
+
),
|
|
374
|
+
policy_field(
|
|
375
|
+
"rate_limit_exempt",
|
|
376
|
+
CIDR_LIST,
|
|
377
|
+
"Rate-limit exempt networks",
|
|
378
|
+
"Addresses never counted against the rate limit (monitors, the "
|
|
379
|
+
"office, a partner integration).",
|
|
380
|
+
placeholder="192.0.2.0/24",
|
|
381
|
+
),
|
|
382
|
+
)
|
|
315
383
|
schema = {
|
|
316
384
|
EVENTS_TABLE: {
|
|
317
385
|
"guard": "TEXT",
|
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity/aws/capabilities/single_session.py
RENAMED
|
@@ -26,6 +26,13 @@ SESSION_TABLE = "app_session"
|
|
|
26
26
|
@capability
|
|
27
27
|
class SingleSession(Capability):
|
|
28
28
|
name = "single_session"
|
|
29
|
+
label = "Single session per user"
|
|
30
|
+
description = (
|
|
31
|
+
"One active login per user: a new sign-in ends the older session. "
|
|
32
|
+
"Enforcement and the staleness window are per-environment flags; the "
|
|
33
|
+
"reaper deletes sessions not seen for the retention window. No "
|
|
34
|
+
"per-site policy."
|
|
35
|
+
)
|
|
29
36
|
defaults = {
|
|
30
37
|
"single_session_enforcement": "true",
|
|
31
38
|
"session_stale_minutes": "10",
|
{velocity_python-0.1.106 → velocity_python-0.1.107}/src/velocity_python.egg-info/SOURCES.txt
RENAMED
|
@@ -211,6 +211,7 @@ tests/test_async_support.py
|
|
|
211
211
|
tests/test_batch_operations.py
|
|
212
212
|
tests/test_blob_store.py
|
|
213
213
|
tests/test_capabilities.py
|
|
214
|
+
tests/test_capability_catalog.py
|
|
214
215
|
tests/test_column_tx_arg.py
|
|
215
216
|
tests/test_concurrency_safety.py
|
|
216
217
|
tests/test_connection_pool.py
|