velocity-python 0.1.105__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.105 → velocity_python-0.1.107}/PKG-INFO +1 -1
- {velocity_python-0.1.105 → velocity_python-0.1.107}/pyproject.toml +1 -1
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.107/src/velocity/aws/capabilities/__init__.py +60 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/base.py +254 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/dispatch.py +61 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/infra.py +121 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/jobs.py +59 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/provisioning.py +180 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/retention.py +82 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/security.py +419 -0
- velocity_python-0.1.107/src/velocity/aws/capabilities/single_session.py +67 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/feature_flags.py +64 -14
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/base_handler.py +22 -1
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/context.py +10 -7
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/lambda_handler.py +15 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/job_handler.py +13 -28
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/session_control.py +39 -11
- velocity_python-0.1.107/src/velocity/aws/job_lifecycle.py +46 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/table.py +33 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/SOURCES.txt +18 -0
- velocity_python-0.1.107/tests/test_access_error_403.py +68 -0
- velocity_python-0.1.107/tests/test_capabilities.py +363 -0
- velocity_python-0.1.107/tests/test_capability_catalog.py +188 -0
- velocity_python-0.1.107/tests/test_context_flags_loader.py +68 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_feature_flags.py +17 -4
- velocity_python-0.1.107/tests/test_job_lifecycle.py +102 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_lambda_handler_masquerade.py +35 -2
- velocity_python-0.1.107/tests/test_security_capability.py +307 -0
- velocity_python-0.1.107/tests/test_single_session_capability.py +84 -0
- velocity_python-0.1.107/tests/test_table_purge.py +75 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/LICENSE +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/README.md +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/setup.cfg +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_blob_store.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_observability.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_response.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_session_control.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.105 → velocity_python-0.1.107}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""System-level capabilities a site turns on: declared once in code, toggled
|
|
2
|
+
per site/environment by the control plane, materialised by
|
|
3
|
+
:func:`provisioning.provision` at deploy time and run by
|
|
4
|
+
:class:`dispatch.CapabilityDispatcher` at runtime. See :mod:`base`.
|
|
5
|
+
|
|
6
|
+
Importing this package registers the built-in capabilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .base import ( # noqa: F401
|
|
10
|
+
EVERY_15_MINUTES,
|
|
11
|
+
HOURLY,
|
|
12
|
+
NIGHTLY,
|
|
13
|
+
POLICY_KINDS,
|
|
14
|
+
REGISTRY,
|
|
15
|
+
Capability,
|
|
16
|
+
capability,
|
|
17
|
+
catalog,
|
|
18
|
+
describe,
|
|
19
|
+
get,
|
|
20
|
+
load_config,
|
|
21
|
+
policy_field,
|
|
22
|
+
resolve_config,
|
|
23
|
+
validate_policy,
|
|
24
|
+
)
|
|
25
|
+
from .dispatch import ( # noqa: F401
|
|
26
|
+
CapabilityCatalogMixin,
|
|
27
|
+
CapabilityDispatcher,
|
|
28
|
+
run_capability,
|
|
29
|
+
)
|
|
30
|
+
from .provisioning import provision, trigger_key # noqa: F401
|
|
31
|
+
|
|
32
|
+
# built-ins (importing registers them)
|
|
33
|
+
from . import retention # noqa: E402,F401
|
|
34
|
+
from . import security # noqa: E402,F401
|
|
35
|
+
from . import infra # noqa: E402,F401
|
|
36
|
+
from . import single_session # noqa: E402,F401
|
|
37
|
+
from . import jobs # noqa: E402,F401
|
|
38
|
+
from .security import SecurityMixin # noqa: E402,F401
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Capability",
|
|
42
|
+
"CapabilityCatalogMixin",
|
|
43
|
+
"CapabilityDispatcher",
|
|
44
|
+
"POLICY_KINDS",
|
|
45
|
+
"REGISTRY",
|
|
46
|
+
"capability",
|
|
47
|
+
"catalog",
|
|
48
|
+
"describe",
|
|
49
|
+
"get",
|
|
50
|
+
"load_config",
|
|
51
|
+
"policy_field",
|
|
52
|
+
"provision",
|
|
53
|
+
"resolve_config",
|
|
54
|
+
"run_capability",
|
|
55
|
+
"trigger_key",
|
|
56
|
+
"validate_policy",
|
|
57
|
+
"NIGHTLY",
|
|
58
|
+
"HOURLY",
|
|
59
|
+
"EVERY_15_MINUTES",
|
|
60
|
+
]
|
|
@@ -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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Runtime dispatch: route the ``run-capability`` queue action to a capability.
|
|
2
|
+
|
|
3
|
+
A site's queue handler mixes in :class:`CapabilityDispatcher`; the scheduler
|
|
4
|
+
stubs seeded by :mod:`provision` enqueue ``run-capability`` with
|
|
5
|
+
``{"capability": name, "action": action}`` and this routes it. Config is
|
|
6
|
+
resolved locally — the site's own ``sys_capabilities`` row plus its feature
|
|
7
|
+
flags — so the runtime never reaches into the control plane.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from velocity.aws import feature_flags
|
|
15
|
+
|
|
16
|
+
from .base import REGISTRY, catalog, resolve_config
|
|
17
|
+
from .provisioning import SITE_TABLE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_capability(tx, context, name, action):
|
|
21
|
+
"""Resolve config and run one capability action. Returns the response body."""
|
|
22
|
+
cap = REGISTRY.get(name)
|
|
23
|
+
if cap is None:
|
|
24
|
+
return {"error": f"unknown capability {name!r}", "capability": name}
|
|
25
|
+
|
|
26
|
+
row = tx.table(SITE_TABLE).find({"name": name}) or {}
|
|
27
|
+
if row and row.get("enabled") is False:
|
|
28
|
+
return {
|
|
29
|
+
"capability": name,
|
|
30
|
+
"action": action,
|
|
31
|
+
"skipped": "disabled for this site",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
raw = row.get("config") if row else None
|
|
35
|
+
row_config = json.loads(raw) if isinstance(raw, str) and raw else (raw or {})
|
|
36
|
+
config = resolve_config(cap, flags=feature_flags.get(tx), row_config=row_config)
|
|
37
|
+
|
|
38
|
+
result = cap.handle(action, tx, context, config)
|
|
39
|
+
return {"capability": name, "action": action, "result": result}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CapabilityDispatcher:
|
|
43
|
+
"""Queue-handler mixin: ``run-capability`` -> the registered capability."""
|
|
44
|
+
|
|
45
|
+
def OnActionRunCapability(self, tx, context):
|
|
46
|
+
payload = context.payload() or {}
|
|
47
|
+
body = run_capability(
|
|
48
|
+
tx, context, payload.get("capability"), payload.get("action")
|
|
49
|
+
)
|
|
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
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""jobs: the scheduled half of asynchronous-job hygiene.
|
|
2
|
+
|
|
3
|
+
``JobHandler`` only discovers a stale job (worker died: Lambda timeout / OOM)
|
|
4
|
+
when someone polls its status — an abandoned job nobody polls stays "Running"
|
|
5
|
+
forever, and ``aws_job_activity`` is never pruned. This capability sweeps on
|
|
6
|
+
a schedule with the SAME predicate the web tier uses
|
|
7
|
+
(``velocity.aws.job_lifecycle.job_is_stale``): stale jobs are persisted as
|
|
8
|
+
Failed with the standard message, and old activity rows are purged.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import datetime
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from velocity.aws.job_lifecycle import (
|
|
17
|
+
JOB_ACTIVITY_TABLE,
|
|
18
|
+
STALE_FAILURE,
|
|
19
|
+
STALE_JOB_STATUSES,
|
|
20
|
+
job_is_stale,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .base import EVERY_15_MINUTES, Capability, capability
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@capability
|
|
27
|
+
class Jobs(Capability):
|
|
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
|
+
)
|
|
35
|
+
defaults = {"job_stale_minutes": "16", "job_retention_days": "30"}
|
|
36
|
+
schedules = (("expire_stale", EVERY_15_MINUTES),)
|
|
37
|
+
|
|
38
|
+
def handle(self, action, tx, context, config):
|
|
39
|
+
if action != "expire_stale":
|
|
40
|
+
raise ValueError(f"jobs: unknown action {action!r}")
|
|
41
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
42
|
+
stale_after = datetime.timedelta(
|
|
43
|
+
minutes=int(config.get("job_stale_minutes") or 16)
|
|
44
|
+
)
|
|
45
|
+
jobs = tx.table(JOB_ACTIVITY_TABLE)
|
|
46
|
+
|
|
47
|
+
expired = 0
|
|
48
|
+
for status in sorted(STALE_JOB_STATUSES):
|
|
49
|
+
for job in jobs.select(where={"status": status}):
|
|
50
|
+
if job_is_stale(job, now=now, stale_after=stale_after):
|
|
51
|
+
jobs.update(
|
|
52
|
+
{"status": "Failed", "message": json.dumps(STALE_FAILURE)},
|
|
53
|
+
where={"sys_id": job["sys_id"]},
|
|
54
|
+
)
|
|
55
|
+
expired += 1
|
|
56
|
+
|
|
57
|
+
days = int(config.get("job_retention_days") or 30)
|
|
58
|
+
purged = jobs.purge({"<sys_created": now - datetime.timedelta(days=days)})
|
|
59
|
+
return {"expired": expired, "purged": purged}
|