velocity-python 0.1.109__tar.gz → 0.1.111__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.109/src/velocity_python.egg-info → velocity_python-0.1.111}/PKG-INFO +1 -1
- {velocity_python-0.1.109 → velocity_python-0.1.111}/pyproject.toml +1 -1
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/base.py +14 -4
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/security.py +11 -11
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/feature_flags.py +21 -20
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/context.py +94 -45
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/lambda_handler.py +26 -5
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/data_service.py +11 -2
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/mirror.py +3 -1
- velocity_python-0.1.111/src/velocity/cache.py +188 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/table.py +130 -16
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/transaction.py +7 -1
- {velocity_python-0.1.109 → velocity_python-0.1.111/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/SOURCES.txt +5 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_capability_catalog.py +6 -1
- velocity_python-0.1.111/tests/test_cognito_user_cache.py +93 -0
- velocity_python-0.1.111/tests/test_lambda_handler_user_cache.py +87 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_mirror.py +35 -10
- velocity_python-0.1.111/tests/test_read_cache.py +119 -0
- velocity_python-0.1.111/tests/test_table_find_cache.py +178 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/LICENSE +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/README.md +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/setup.cfg +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/dispatch.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/infra.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/jobs.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/provisioning.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/retention.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/single_session.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/job_lifecycle.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_access_error_403.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_blob_store.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_capabilities.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_context_flags_loader.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_feature_flags.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_lifecycle.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_observability.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_response.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_security_capability.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_session_control.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_single_session_capability.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_table_purge.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -55,7 +55,16 @@ TEXT = "text"
|
|
|
55
55
|
BOOLEAN = "boolean"
|
|
56
56
|
CHOICE = "choice" # one of `options`
|
|
57
57
|
LIST = "list" # free strings
|
|
58
|
-
POLICY_KINDS = (
|
|
58
|
+
POLICY_KINDS = (
|
|
59
|
+
CIDR_LIST,
|
|
60
|
+
COUNTRY_LIST,
|
|
61
|
+
TABLE_DAYS,
|
|
62
|
+
NUMBER,
|
|
63
|
+
TEXT,
|
|
64
|
+
BOOLEAN,
|
|
65
|
+
CHOICE,
|
|
66
|
+
LIST,
|
|
67
|
+
)
|
|
59
68
|
|
|
60
69
|
IDENTIFIER = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
61
70
|
COUNTRY_CODE = re.compile(r"^[A-Z]{2}$")
|
|
@@ -145,7 +154,9 @@ def describe(cap) -> dict:
|
|
|
145
154
|
def catalog() -> list:
|
|
146
155
|
"""Every registered capability, described, ordered by label — the
|
|
147
156
|
pick-list an admin UI shows."""
|
|
148
|
-
return sorted(
|
|
157
|
+
return sorted(
|
|
158
|
+
(describe(cap) for cap in REGISTRY.values()), key=lambda d: d["label"]
|
|
159
|
+
)
|
|
149
160
|
|
|
150
161
|
|
|
151
162
|
def load_config(raw) -> dict:
|
|
@@ -232,8 +243,7 @@ def validate_policy(name, config) -> list:
|
|
|
232
243
|
if value in (None, "", [], {}):
|
|
233
244
|
continue
|
|
234
245
|
problems.extend(
|
|
235
|
-
f"{field['label']}: {problem}"
|
|
236
|
-
for problem in _check_field(field, value)
|
|
246
|
+
f"{field['label']}: {problem}" for problem in _check_field(field, value)
|
|
237
247
|
)
|
|
238
248
|
return problems
|
|
239
249
|
|
{velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/security.py
RENAMED
|
@@ -27,9 +27,9 @@ from __future__ import annotations
|
|
|
27
27
|
import datetime
|
|
28
28
|
import ipaddress
|
|
29
29
|
import logging
|
|
30
|
-
import time
|
|
31
30
|
|
|
32
31
|
from velocity.aws import feature_flags
|
|
32
|
+
from velocity.cache import memo, read_cache
|
|
33
33
|
|
|
34
34
|
from .base import (
|
|
35
35
|
CIDR_LIST,
|
|
@@ -62,12 +62,10 @@ KNOBS = {
|
|
|
62
62
|
"security_events_days": "90",
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
_cache: dict = {}
|
|
66
|
-
|
|
67
65
|
|
|
68
66
|
def clear_cache():
|
|
69
67
|
"""Forget the cached site row AND the shared flags row (tests)."""
|
|
70
|
-
|
|
68
|
+
read_cache.invalidate(SITE_TABLE)
|
|
71
69
|
feature_flags.clear_cache()
|
|
72
70
|
|
|
73
71
|
|
|
@@ -174,13 +172,15 @@ def check_rate(tx, bucket, *, limit, now, table=RATE_TABLE):
|
|
|
174
172
|
|
|
175
173
|
|
|
176
174
|
def _cached(key, loader, *, now=None):
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
"""The process read cache (velocity.cache), tagged with the site table
|
|
176
|
+
so provisioning's own writes to it in this process drop the entry."""
|
|
177
|
+
return memo(
|
|
178
|
+
("security",) + tuple(key),
|
|
179
|
+
loader,
|
|
180
|
+
CACHE_TTL_SECONDS,
|
|
181
|
+
tables=(SITE_TABLE,),
|
|
182
|
+
now=now,
|
|
183
|
+
)
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
def _site_row(tx):
|
|
@@ -32,9 +32,9 @@ mechanism applied to the flags table, keyed by ``applied_system``.
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
import logging
|
|
35
|
-
import time
|
|
36
35
|
|
|
37
36
|
from velocity.aws import mirror as _mirror
|
|
37
|
+
from velocity.cache import memo, read_cache
|
|
38
38
|
|
|
39
39
|
log = logging.getLogger(__name__)
|
|
40
40
|
|
|
@@ -46,7 +46,6 @@ CACHE_SECONDS_CONFIG_KEY = "FeatureFlagCacheSeconds"
|
|
|
46
46
|
DEFAULT_CACHE_SECONDS = 60
|
|
47
47
|
|
|
48
48
|
_alternate_engines = _mirror._alternate_engines
|
|
49
|
-
_cache: dict = {} # (system, table) -> (expires_at, row)
|
|
50
49
|
_cache_seconds: float | None = None
|
|
51
50
|
|
|
52
51
|
|
|
@@ -102,37 +101,39 @@ def cache_seconds() -> float:
|
|
|
102
101
|
return _cache_seconds
|
|
103
102
|
|
|
104
103
|
|
|
105
|
-
def clear_cache() -> None:
|
|
104
|
+
def clear_cache(table: str = DEFAULT_TABLE) -> None:
|
|
106
105
|
"""Forget cached rows (after a local flags write; tests)."""
|
|
107
106
|
global _cache_seconds
|
|
108
|
-
|
|
107
|
+
read_cache.invalidate(table)
|
|
109
108
|
_cache_seconds = None
|
|
110
109
|
|
|
111
110
|
|
|
112
111
|
def get_cached(
|
|
113
|
-
tx,
|
|
112
|
+
tx,
|
|
113
|
+
system: str | None = None,
|
|
114
|
+
*,
|
|
115
|
+
table: str = DEFAULT_TABLE,
|
|
116
|
+
now: float | None = None,
|
|
114
117
|
) -> dict:
|
|
115
|
-
""":func:`get`
|
|
118
|
+
""":func:`get` through the process read cache (:mod:`velocity.cache`)
|
|
119
|
+
— the hot-path reader.
|
|
116
120
|
|
|
117
121
|
The flags row is ONE row per stage, and every reader in a request
|
|
118
122
|
(the handler context, the security guards, session control, any
|
|
119
123
|
capability knob) wants the same one: this makes it at most one read per
|
|
120
|
-
:func:`cache_seconds` per container, however many readers there are.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sees
|
|
124
|
+
:func:`cache_seconds` per container, however many readers there are.
|
|
125
|
+
Tagged with the table, so a flags write through the table API in this
|
|
126
|
+
process (the mirror hook, a save) drops it at once; the other
|
|
127
|
+
environment sees the change within the TTL.
|
|
124
128
|
"""
|
|
125
129
|
system = system if system is not None else current_system()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return dict(hit[1])
|
|
134
|
-
row = get(tx, system, table=table)
|
|
135
|
-
_cache[key] = (now + ttl, dict(row))
|
|
130
|
+
row = memo(
|
|
131
|
+
("feature_flags", table, system),
|
|
132
|
+
lambda: dict(get(tx, system, table=table)),
|
|
133
|
+
cache_seconds(),
|
|
134
|
+
tables=(table,),
|
|
135
|
+
now=now,
|
|
136
|
+
)
|
|
136
137
|
return dict(row)
|
|
137
138
|
|
|
138
139
|
|
|
@@ -17,6 +17,7 @@ from velocity.aws.ssm_config import get_project_name as config_get_project_name
|
|
|
17
17
|
from velocity.aws.ssm_config import get_region as config_get_region
|
|
18
18
|
from velocity.aws.ssm_config import get_stage as config_get_stage
|
|
19
19
|
from velocity.logging import get_logger
|
|
20
|
+
from velocity.cache import memo
|
|
20
21
|
|
|
21
22
|
engine = velocity.db.postgres.initialize()
|
|
22
23
|
|
|
@@ -81,6 +82,40 @@ def _get_controls_vars() -> dict:
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
# ── the signed-in user, cached per container ─────────────────────────────
|
|
86
|
+
USER_CACHE_CONFIG_KEY = "UserCacheSeconds"
|
|
87
|
+
DEFAULT_USER_CACHE_SECONDS = 60
|
|
88
|
+
_user_cache_seconds = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def user_cache_seconds() -> float:
|
|
92
|
+
"""Seconds a warm container may reuse a signed-in user's Cognito record and
|
|
93
|
+
user-table row: the ``UserCacheSeconds`` config key, default 60. A disabled
|
|
94
|
+
or deleted user keeps that container's access until it expires — accepted
|
|
95
|
+
policy (Paul, 2026-09-13). ``0`` looks the user up on every request."""
|
|
96
|
+
global _user_cache_seconds
|
|
97
|
+
if _user_cache_seconds is None:
|
|
98
|
+
raw = None
|
|
99
|
+
try:
|
|
100
|
+
raw = config_getenv(USER_CACHE_CONFIG_KEY, None)
|
|
101
|
+
except Exception:
|
|
102
|
+
raw = None
|
|
103
|
+
try:
|
|
104
|
+
_user_cache_seconds = (
|
|
105
|
+
float(raw)
|
|
106
|
+
if raw not in (None, "")
|
|
107
|
+
else float(DEFAULT_USER_CACHE_SECONDS)
|
|
108
|
+
)
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
_user_cache_seconds = float(DEFAULT_USER_CACHE_SECONDS)
|
|
111
|
+
return _user_cache_seconds
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def clear_user_cache_config() -> None:
|
|
115
|
+
global _user_cache_seconds
|
|
116
|
+
_user_cache_seconds = None
|
|
117
|
+
|
|
118
|
+
|
|
84
119
|
@engine.transaction
|
|
85
120
|
class Context:
|
|
86
121
|
|
|
@@ -807,63 +842,77 @@ class Context:
|
|
|
807
842
|
"cannot resolve a Cognito user"
|
|
808
843
|
) from None
|
|
809
844
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
UserPoolId=user_pool_id,
|
|
814
|
-
Username=user_sub,
|
|
815
|
-
)
|
|
816
|
-
self.perf.log("cognito admin_get_user")
|
|
817
|
-
resolved_username = response.get("Username")
|
|
818
|
-
except cognito_client.exceptions.UserNotFoundException:
|
|
845
|
+
# one Cognito round trip per user per UserCacheSeconds per container
|
|
846
|
+
# (velocity.cache); a lookup that fails is never cached
|
|
847
|
+
def resolve():
|
|
819
848
|
try:
|
|
820
|
-
self.perf.start("cognito
|
|
821
|
-
|
|
849
|
+
self.perf.start("cognito admin_get_user")
|
|
850
|
+
response = cognito_client.admin_get_user(
|
|
822
851
|
UserPoolId=user_pool_id,
|
|
823
|
-
|
|
824
|
-
Limit=1,
|
|
852
|
+
Username=user_sub,
|
|
825
853
|
)
|
|
826
|
-
self.perf.log("cognito
|
|
854
|
+
self.perf.log("cognito admin_get_user")
|
|
855
|
+
resolved_username = response.get("Username")
|
|
856
|
+
except cognito_client.exceptions.UserNotFoundException:
|
|
857
|
+
try:
|
|
858
|
+
self.perf.start("cognito list_users")
|
|
859
|
+
list_response = cognito_client.list_users(
|
|
860
|
+
UserPoolId=user_pool_id,
|
|
861
|
+
Filter=f'sub = "{user_sub}"',
|
|
862
|
+
Limit=1,
|
|
863
|
+
)
|
|
864
|
+
self.perf.log("cognito list_users")
|
|
865
|
+
except ClientError as exc:
|
|
866
|
+
message = exc.response.get("Error", {}).get(
|
|
867
|
+
"Message", "Unknown error"
|
|
868
|
+
)
|
|
869
|
+
raise AlertError(
|
|
870
|
+
f"Failed to search Cognito user by sub: {message}"
|
|
871
|
+
) from None
|
|
872
|
+
|
|
873
|
+
users = list_response.get("Users", [])
|
|
874
|
+
if not users:
|
|
875
|
+
raise AlertError("Cognito user not found") from None
|
|
876
|
+
|
|
877
|
+
user = users[0]
|
|
878
|
+
response = {
|
|
879
|
+
"Username": user.get("Username"),
|
|
880
|
+
"UserAttributes": user.get("Attributes", []),
|
|
881
|
+
"Enabled": user.get("Enabled"),
|
|
882
|
+
"UserStatus": user.get("UserStatus"),
|
|
883
|
+
"UserCreateDate": user.get("UserCreateDate"),
|
|
884
|
+
"UserLastModifiedDate": user.get("UserLastModifiedDate"),
|
|
885
|
+
}
|
|
886
|
+
resolved_username = response.get("Username")
|
|
827
887
|
except ClientError as exc:
|
|
828
888
|
message = exc.response.get("Error", {}).get("Message", "Unknown error")
|
|
829
889
|
raise AlertError(
|
|
830
|
-
f"Failed to
|
|
890
|
+
f"Failed to retrieve Cognito user: {message}"
|
|
831
891
|
) from None
|
|
832
892
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
user = users[0]
|
|
838
|
-
response = {
|
|
839
|
-
"Username": user.get("Username"),
|
|
840
|
-
"UserAttributes": user.get("Attributes", []),
|
|
841
|
-
"Enabled": user.get("Enabled"),
|
|
842
|
-
"UserStatus": user.get("UserStatus"),
|
|
843
|
-
"UserCreateDate": user.get("UserCreateDate"),
|
|
844
|
-
"UserLastModifiedDate": user.get("UserLastModifiedDate"),
|
|
893
|
+
attributes = {
|
|
894
|
+
attr["Name"]: attr.get("Value")
|
|
895
|
+
for attr in response.get("UserAttributes", [])
|
|
845
896
|
}
|
|
846
|
-
resolved_username = response.get("Username")
|
|
847
|
-
except ClientError as exc:
|
|
848
|
-
message = exc.response.get("Error", {}).get("Message", "Unknown error")
|
|
849
|
-
raise AlertError(f"Failed to retrieve Cognito user: {message}") from None
|
|
850
|
-
|
|
851
|
-
attributes = {
|
|
852
|
-
attr["Name"]: attr.get("Value")
|
|
853
|
-
for attr in response.get("UserAttributes", [])
|
|
854
|
-
}
|
|
855
897
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
898
|
+
user = {
|
|
899
|
+
"username": resolved_username or user_sub,
|
|
900
|
+
"email": attributes.get("email"),
|
|
901
|
+
"attributes": attributes,
|
|
902
|
+
"enabled": response.get("Enabled"),
|
|
903
|
+
"user_status": response.get("UserStatus"),
|
|
904
|
+
"sub": attributes.get("sub", user_sub),
|
|
905
|
+
}
|
|
864
906
|
|
|
865
|
-
|
|
907
|
+
return user
|
|
866
908
|
|
|
909
|
+
user = memo(
|
|
910
|
+
("cognito", user_pool_id, user_sub),
|
|
911
|
+
resolve,
|
|
912
|
+
user_cache_seconds(),
|
|
913
|
+
cache_none=False,
|
|
914
|
+
)
|
|
915
|
+
self.perf.log("get_cognito_user total")
|
|
867
916
|
return user
|
|
868
917
|
|
|
869
918
|
def get_cognito_user_optional(self, aws_event):
|
{velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/lambda_handler.py
RENAMED
|
@@ -161,15 +161,13 @@ class LambdaHandler(BaseHandler):
|
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
context.perf.start("user lookup")
|
|
164
|
-
|
|
165
|
-
{"email_address": session.get("email_address")}
|
|
166
|
-
)
|
|
164
|
+
user = self._cached_user(tx, session.get("email_address"))
|
|
167
165
|
context.perf.log("user lookup")
|
|
168
|
-
if not
|
|
166
|
+
if not user:
|
|
169
167
|
raise Exception(
|
|
170
168
|
"A valid user with permission is required to access this function [DB]"
|
|
171
169
|
)
|
|
172
|
-
self.current_user =
|
|
170
|
+
self.current_user = user
|
|
173
171
|
|
|
174
172
|
temp = copy.deepcopy(context.postdata() or {})
|
|
175
173
|
payload = temp.get("payload")
|
|
@@ -181,6 +179,29 @@ class LambdaHandler(BaseHandler):
|
|
|
181
179
|
extra={"payload": pprint.pformat(temp)},
|
|
182
180
|
)
|
|
183
181
|
|
|
182
|
+
def _cached_user(self, tx, email_address):
|
|
183
|
+
"""The user-table row for the signed-in user as a dict, through the
|
|
184
|
+
process read cache (velocity.cache) for ``UserCacheSeconds``: one
|
|
185
|
+
query per TTL per container instead of one per request. A miss is
|
|
186
|
+
never cached, so a user created a moment ago signs in at once; a
|
|
187
|
+
write to the user table in this process drops the entry."""
|
|
188
|
+
from velocity.cache import memo
|
|
189
|
+
|
|
190
|
+
from velocity.aws.handlers.context import user_cache_seconds
|
|
191
|
+
|
|
192
|
+
def load():
|
|
193
|
+
row = tx.table(self.user_table).find({"email_address": email_address})
|
|
194
|
+
return row.to_dict() if row else None
|
|
195
|
+
|
|
196
|
+
user = memo(
|
|
197
|
+
("user", self.user_table, email_address),
|
|
198
|
+
load,
|
|
199
|
+
user_cache_seconds(),
|
|
200
|
+
tables=(self.user_table,),
|
|
201
|
+
cache_none=False,
|
|
202
|
+
)
|
|
203
|
+
return dict(user) if user else None
|
|
204
|
+
|
|
184
205
|
def afterAction(self, tx, context):
|
|
185
206
|
self._enhanced_after_action(tx, context)
|
|
186
207
|
logger.debug("starting LamdaHandler.afterAction")
|
{velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -20,6 +20,10 @@ from velocity.aws.handlers.exceptions import AlertError
|
|
|
20
20
|
from velocity.db.exceptions import DbDataIntegrityError
|
|
21
21
|
from velocity.misc import export
|
|
22
22
|
|
|
23
|
+
# a table's column list changes only on deploy (schema sync) or a lazy ALTER
|
|
24
|
+
# in this process (which drops the entry): reuse it across requests
|
|
25
|
+
SCHEMA_CACHE_SECONDS = 300
|
|
26
|
+
|
|
23
27
|
logger = logging.getLogger(__name__)
|
|
24
28
|
|
|
25
29
|
# Postgres FK-violation text, e.g.:
|
|
@@ -522,7 +526,9 @@ class DataServiceMixin:
|
|
|
522
526
|
order_dir = parts[1].lower()
|
|
523
527
|
|
|
524
528
|
if not order_col:
|
|
525
|
-
available = [
|
|
529
|
+
available = [
|
|
530
|
+
c for c in tx.table(obj).sys_columns(cache_ttl=SCHEMA_CACHE_SECONDS)
|
|
531
|
+
]
|
|
526
532
|
lowered = {c.lower(): c for c in available}
|
|
527
533
|
order_col = lowered.get("sys_id") or lowered.get("sys_created")
|
|
528
534
|
if not order_col and available:
|
|
@@ -551,7 +557,10 @@ class DataServiceMixin:
|
|
|
551
557
|
_inject_row_number_expression(params)
|
|
552
558
|
requested_cols = _extract_requested_column_names(params.get("columns"))
|
|
553
559
|
if requested_cols:
|
|
554
|
-
available_cols = {
|
|
560
|
+
available_cols = {
|
|
561
|
+
c.lower()
|
|
562
|
+
for c in tx.table(obj).sys_columns(cache_ttl=SCHEMA_CACHE_SECONDS)
|
|
563
|
+
}
|
|
555
564
|
missing = sorted(
|
|
556
565
|
{c for c in requested_cols if c.lower() not in available_cols},
|
|
557
566
|
key=lambda v: v.lower(),
|
|
@@ -176,7 +176,9 @@ def _run(operation, context, what, verbs, notify):
|
|
|
176
176
|
except MirrorSkip as skip:
|
|
177
177
|
log.warning("%s not %s the alternate database: %s", what, done, skip)
|
|
178
178
|
if notify:
|
|
179
|
-
toast(
|
|
179
|
+
toast(
|
|
180
|
+
context, f"{what} {undone} the alternate database: {skip}.", "warning"
|
|
181
|
+
)
|
|
180
182
|
return None
|
|
181
183
|
except Exception as exc: # never block the local write
|
|
182
184
|
log.warning("%s: mirror to the alternate database failed", what, exc_info=True)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Process-level read cache: reads that may be reused across requests.
|
|
2
|
+
|
|
3
|
+
velocity's transaction cache (``select(..., cache=True)``) lives and dies
|
|
4
|
+
with one transaction. This store outlives requests on a warm Lambda
|
|
5
|
+
container (or any long-lived process), so a read that is the same for
|
|
6
|
+
every request — a feature-flag row, a capability row, the signed-in user,
|
|
7
|
+
a table's column list, a Cognito lookup — costs one round trip per TTL
|
|
8
|
+
instead of one per request. One store, one policy, no per-module dicts.
|
|
9
|
+
|
|
10
|
+
Three ways in:
|
|
11
|
+
|
|
12
|
+
* table reads — ``tx.table(t).find(where, cache_ttl=60)``,
|
|
13
|
+
``exists(cache_ttl=60)``, ``sys_columns(cache_ttl=300)``,
|
|
14
|
+
``select(..., cache_ttl=60)`` — keyed by database + SQL + params;
|
|
15
|
+
* :func:`memo` — any loader (an API call, a composite of reads), tagged
|
|
16
|
+
with the tables it depends on;
|
|
17
|
+
* :data:`read_cache` — explicit ``invalidate`` / ``clear`` / ``stats``.
|
|
18
|
+
|
|
19
|
+
**Invalidation.** A write through the table API (insert / update / delete /
|
|
20
|
+
merge / upsert / truncate) or DDL on table *T* in this process drops every
|
|
21
|
+
entry tagged *T* at once; a raw ``tx.execute`` write is classified and does
|
|
22
|
+
the same. Other processes see the change when their TTL expires — that
|
|
23
|
+
bounded staleness is the contract a caller accepts by naming a TTL.
|
|
24
|
+
``VELOCITY_READ_CACHE=0`` disables the store everywhere (tests, debugging).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import threading
|
|
31
|
+
import time
|
|
32
|
+
from collections import OrderedDict
|
|
33
|
+
|
|
34
|
+
ENV_KEY = "VELOCITY_READ_CACHE"
|
|
35
|
+
MAX_ENTRIES = 1024
|
|
36
|
+
# tag for entries that describe the schema (table list, columns): DDL drops
|
|
37
|
+
# them; ordinary writes do not
|
|
38
|
+
SCHEMA_TAG = "__schema__"
|
|
39
|
+
|
|
40
|
+
_MISS = object()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _norm(table) -> str:
|
|
44
|
+
return str(table).strip().lower()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReadCache:
|
|
48
|
+
"""A TTL + LRU store of read results, indexed by the tables they depend on."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, max_entries: int = MAX_ENTRIES):
|
|
51
|
+
self._entries: OrderedDict = OrderedDict() # key -> (expires_at, value, tags)
|
|
52
|
+
self._by_table: dict = {} # tag -> set(keys)
|
|
53
|
+
self._lock = threading.RLock()
|
|
54
|
+
self._max = max_entries
|
|
55
|
+
self._enabled: bool | None = None
|
|
56
|
+
self.hits = 0
|
|
57
|
+
self.misses = 0
|
|
58
|
+
|
|
59
|
+
# ── switch ──────────────────────────────────────────────────────────
|
|
60
|
+
def enabled(self) -> bool:
|
|
61
|
+
if self._enabled is None:
|
|
62
|
+
raw = os.environ.get(ENV_KEY, "").strip().lower()
|
|
63
|
+
self._enabled = raw not in ("0", "false", "off", "no")
|
|
64
|
+
return self._enabled
|
|
65
|
+
|
|
66
|
+
def configure(self, *, enabled: bool | None) -> None:
|
|
67
|
+
"""Force the switch (``None`` re-reads the environment)."""
|
|
68
|
+
self._enabled = enabled
|
|
69
|
+
|
|
70
|
+
# ── core ────────────────────────────────────────────────────────────
|
|
71
|
+
def get(self, key, *, now: float | None = None):
|
|
72
|
+
"""``(hit, value)``; an expired entry is a miss and is dropped."""
|
|
73
|
+
if not self.enabled():
|
|
74
|
+
return False, None
|
|
75
|
+
now = time.monotonic() if now is None else now
|
|
76
|
+
with self._lock:
|
|
77
|
+
entry = self._entries.get(key)
|
|
78
|
+
if entry is None:
|
|
79
|
+
self.misses += 1
|
|
80
|
+
return False, None
|
|
81
|
+
expires_at, value, _tags = entry
|
|
82
|
+
if expires_at <= now:
|
|
83
|
+
self._drop(key)
|
|
84
|
+
self.misses += 1
|
|
85
|
+
return False, None
|
|
86
|
+
self._entries.move_to_end(key)
|
|
87
|
+
self.hits += 1
|
|
88
|
+
return True, value
|
|
89
|
+
|
|
90
|
+
def put(
|
|
91
|
+
self, key, value, ttl: float, *, tables=(), now: float | None = None
|
|
92
|
+
) -> None:
|
|
93
|
+
if not self.enabled() or ttl is None or ttl <= 0:
|
|
94
|
+
return
|
|
95
|
+
now = time.monotonic() if now is None else now
|
|
96
|
+
tags = frozenset(_norm(t) for t in tables if t)
|
|
97
|
+
with self._lock:
|
|
98
|
+
if key in self._entries:
|
|
99
|
+
self._drop(key)
|
|
100
|
+
self._entries[key] = (now + ttl, value, tags)
|
|
101
|
+
for tag in tags:
|
|
102
|
+
self._by_table.setdefault(tag, set()).add(key)
|
|
103
|
+
while len(self._entries) > self._max:
|
|
104
|
+
oldest, _ = self._entries.popitem(last=False)
|
|
105
|
+
self._untag(oldest)
|
|
106
|
+
|
|
107
|
+
def memo(
|
|
108
|
+
self, key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None
|
|
109
|
+
):
|
|
110
|
+
"""``loader()`` once per ``ttl`` for ``key``. ``cache_none=False`` never
|
|
111
|
+
stores a ``None`` result (a "not found" that may exist a moment later)."""
|
|
112
|
+
hit, value = self.get(key, now=now)
|
|
113
|
+
if hit:
|
|
114
|
+
return value
|
|
115
|
+
value = loader()
|
|
116
|
+
if value is not None or cache_none:
|
|
117
|
+
self.put(key, value, ttl, tables=tables, now=now)
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
# ── invalidation ────────────────────────────────────────────────────
|
|
121
|
+
def invalidate(self, table=None) -> int:
|
|
122
|
+
"""Drop every entry tagged ``table`` (all entries when ``None``).
|
|
123
|
+
Returns how many were dropped."""
|
|
124
|
+
with self._lock:
|
|
125
|
+
if table is None:
|
|
126
|
+
count = len(self._entries)
|
|
127
|
+
self._entries.clear()
|
|
128
|
+
self._by_table.clear()
|
|
129
|
+
return count
|
|
130
|
+
keys = list(self._by_table.get(_norm(table), ()))
|
|
131
|
+
for key in keys:
|
|
132
|
+
self._drop(key)
|
|
133
|
+
return len(keys)
|
|
134
|
+
|
|
135
|
+
def invalidate_schema(self) -> int:
|
|
136
|
+
"""DDL happened: drop the table list / column entries."""
|
|
137
|
+
return self.invalidate(SCHEMA_TAG)
|
|
138
|
+
|
|
139
|
+
def clear(self) -> None:
|
|
140
|
+
with self._lock:
|
|
141
|
+
self.invalidate(None)
|
|
142
|
+
self.hits = 0
|
|
143
|
+
self.misses = 0
|
|
144
|
+
|
|
145
|
+
def _drop(self, key) -> None:
|
|
146
|
+
self._entries.pop(key, None)
|
|
147
|
+
self._untag(key)
|
|
148
|
+
|
|
149
|
+
def _untag(self, key) -> None:
|
|
150
|
+
for tag, keys in list(self._by_table.items()):
|
|
151
|
+
keys.discard(key)
|
|
152
|
+
if not keys:
|
|
153
|
+
del self._by_table[tag]
|
|
154
|
+
|
|
155
|
+
# ── observability ───────────────────────────────────────────────────
|
|
156
|
+
def __len__(self) -> int:
|
|
157
|
+
return len(self._entries)
|
|
158
|
+
|
|
159
|
+
def stats(self) -> dict:
|
|
160
|
+
return {
|
|
161
|
+
"enabled": self.enabled(),
|
|
162
|
+
"entries": len(self._entries),
|
|
163
|
+
"hits": self.hits,
|
|
164
|
+
"misses": self.misses,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
read_cache = ReadCache()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def memo(key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None):
|
|
172
|
+
"""Module-level shortcut for :meth:`ReadCache.memo` on the shared store."""
|
|
173
|
+
return read_cache.memo(
|
|
174
|
+
key, loader, ttl, tables=tables, cache_none=cache_none, now=now
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def db_identity(engine) -> str:
|
|
179
|
+
"""A stable name for the database an engine talks to (part of every
|
|
180
|
+
table-read key, so two databases on one instance never share entries)."""
|
|
181
|
+
config = getattr(engine, "config", None)
|
|
182
|
+
if isinstance(config, dict):
|
|
183
|
+
host = config.get("host") or ""
|
|
184
|
+
name = config.get("dbname") or config.get("database") or ""
|
|
185
|
+
return f"{host}/{name}"
|
|
186
|
+
if isinstance(config, str):
|
|
187
|
+
return config
|
|
188
|
+
return f"engine:{id(engine)}"
|