velocity-python 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {velocity_python-0.2.3/src/velocity_python.egg-info → velocity_python-0.2.4}/PKG-INFO +1 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4}/pyproject.toml +1 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/__init__.py +1 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/context.py +3 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/lambda_handler.py +60 -2
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/__init__.py +6 -2
- velocity_python-0.2.4/src/velocity/aws/handlers/mixins/action_names.py +30 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/data_service.py +43 -2
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/permissions_actions.py +6 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/tiles_actions.py +6 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/web_handler.py +8 -0
- velocity_python-0.2.4/src/velocity/aws/identity.py +138 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/permissions.py +189 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/export.py +138 -16
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/format.py +29 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_format.py +30 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity_python.egg-info/SOURCES.txt +5 -0
- velocity_python-0.2.4/tests/test_action_names.py +61 -0
- velocity_python-0.2.4/tests/test_export_workbook.py +422 -0
- velocity_python-0.2.4/tests/test_identity.py +174 -0
- velocity_python-0.2.4/tests/test_permissions_notices.py +259 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_permissions.py +39 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_search_paging_export.py +121 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_store_user_data.py +15 -0
- velocity_python-0.2.3/tests/test_export_workbook.py +0 -164
- {velocity_python-0.2.3 → velocity_python-0.2.4}/LICENSE +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/README.md +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/setup.cfg +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/authz.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/base.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/dispatch.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/infra.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/jobs.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/provisioning.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/retention.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/security.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/capabilities/single_session.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/ci_gate.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/frontend_checks.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/facets.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/failures.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/feature_flags.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/audit_trail.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/client_errors.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/client_input.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/row_scoped.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/job_lifecycle.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/ops.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/queue_defaults.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/schema.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_deploy_least_privilege.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_deploy_policy_size.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/tiles.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/user_data.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/lazy_engine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/fragments.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/tunnel.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/logging.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/_mirror_core.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/money.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/plaid_api.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/plaid_webhook.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/redact.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/schema.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/testing/__init__.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/testing/fake_db.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_access_error_403.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_amplify_redirects.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_asset_references.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_assets_service.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_async_support.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_audit_mixin.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_authz.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_aws_package_hygiene.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_blob_store.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_business_objects.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_capabilities.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_capability_catalog.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_check_changelog.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_ci_gate.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_client_errors_mixin.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_client_input_contract_export.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_client_sql_boundary.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_client_table_gate_case_and_pointers.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_cognito_user_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_context_flags_loader.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_context_queue_job.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_conv_combine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_owner_scope.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_row_number.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_data_service_security_gates.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_db_tunnel.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_declared_column_types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_decorators.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_deploy_config_fields.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_deploy_engine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_deploy_frontend_checks.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_deploy_operations_queue.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_dlq_constant.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_docs_snippets.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_email_processing.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_enqueue_metric_namespace.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_ensure_infrastructure.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_facets.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_facets_and_where.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_failures.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_feature_flags.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_job_handler.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_job_lifecycle.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_job_results.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_json_columns.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_lambda_handler_user_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_lazy_engine.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_merge_on_conflict.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_mirror_upsert_public.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_observability.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_on_action_default.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_on_commit.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_ops.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_audit_fixes.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_business_agnostic.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_charge_reference.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_extra_bounds.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_lazy_imports.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pdf.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_permissions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_permissions_actions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_permissions_review.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_bulk_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_capability_types.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_ensure_infrastructure.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_get_tables.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_pointer_joins.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_row_scope.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_search_export.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_tiles.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pgserver_user_data.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_plaid_api.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_plaid_webhook.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_pointer_join_sql.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_postgres_initializer_config.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_postgres_py_type.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_query_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_raw_sql_prefix_policy.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_read_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_readme_imports.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_response.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_result_returning.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_row_scoped_data_service.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_s3_bucket_config.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_s3_list_client_files.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_s3_upload.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_schema_lock_config_other_backends.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_security_capability.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_session_control.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_single_session_capability.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_sms_twilio_signature.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_ssm_config_require.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_table_alter.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_table_claim.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_table_find_cache.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_table_purge.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_testing_fake_db.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_tiles.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_tx_lifecycle_retry.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_tx_read_cache_and_bulk.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_user_data.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_user_not_provisioned.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_write_hook_create_flow.py +0 -0
- {velocity_python-0.2.3 → velocity_python-0.2.4}/tests/test_write_hook_empty_strings.py +0 -0
|
@@ -1093,6 +1093,9 @@ class Context:
|
|
|
1093
1093
|
"enabled": response.get("Enabled"),
|
|
1094
1094
|
"user_status": response.get("UserStatus"),
|
|
1095
1095
|
"sub": attributes.get("sub", user_sub),
|
|
1096
|
+
# the token issuer for this pool: with "sub", the identity
|
|
1097
|
+
# the provider never reassigns (velocity.aws.identity)
|
|
1098
|
+
"issuer": f"https://{user_pool_segment}",
|
|
1096
1099
|
}
|
|
1097
1100
|
|
|
1098
1101
|
return user
|
|
@@ -177,10 +177,15 @@ class LambdaHandler(BaseHandler):
|
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
context.perf.start("user lookup")
|
|
180
|
-
user = self.
|
|
180
|
+
user = self._find_current_user(tx, session, masquerade)
|
|
181
181
|
context.perf.log("user lookup")
|
|
182
182
|
if not user and not masquerade and _stage_allows_provisioning():
|
|
183
|
-
|
|
183
|
+
if self._by_subject(masquerade):
|
|
184
|
+
# the hook's new row is bound on this, its first, sign-in
|
|
185
|
+
self.provision_missing_user(tx, session)
|
|
186
|
+
user = self._find_current_user(tx, session, masquerade)
|
|
187
|
+
else:
|
|
188
|
+
user = self._provision_and_reload(tx, session)
|
|
184
189
|
if not user:
|
|
185
190
|
raise UserNotProvisionedError(
|
|
186
191
|
"A valid user with permission is required to access this function [DB]"
|
|
@@ -245,6 +250,59 @@ class LambdaHandler(BaseHandler):
|
|
|
245
250
|
)
|
|
246
251
|
return user
|
|
247
252
|
|
|
253
|
+
# How a request finds its user row (velocity.aws.identity): "email" (the
|
|
254
|
+
# default) or "subject" — the identity provider's issuer + subject, the
|
|
255
|
+
# row bound once by verified email on the first sign-in after the switch.
|
|
256
|
+
# The IdentityMode config key (per environment) overrides this.
|
|
257
|
+
identity_mode = "email"
|
|
258
|
+
|
|
259
|
+
def _by_subject(self, masquerade):
|
|
260
|
+
from velocity.aws import identity
|
|
261
|
+
|
|
262
|
+
# a masquerade identity is the grant's user, not a provider subject;
|
|
263
|
+
# the mode is the IdentityMode config key, else identity_mode
|
|
264
|
+
return not masquerade and identity.mode_of(self) == identity.IDENTITY_SUBJECT
|
|
265
|
+
|
|
266
|
+
def _find_current_user(self, tx, session, masquerade=False):
|
|
267
|
+
"""The signed-in caller's user row (dict) or None, by email or — in
|
|
268
|
+
``identity_mode = "subject"`` — by issuer + subject."""
|
|
269
|
+
if not self._by_subject(masquerade):
|
|
270
|
+
return self._cached_user(tx, session.get("email_address"))
|
|
271
|
+
from velocity.aws import identity
|
|
272
|
+
|
|
273
|
+
issuer, subject, email, verified = identity.claims_of(self.cognito_user)
|
|
274
|
+
session["iss"], session["sub"] = issuer, subject
|
|
275
|
+
if not (issuer and subject):
|
|
276
|
+
return None
|
|
277
|
+
return self._cached_subject_user(tx, issuer, subject, email, verified)
|
|
278
|
+
|
|
279
|
+
def _cached_subject_user(self, tx, issuer, subject, email, verified):
|
|
280
|
+
"""``_cached_user`` by issuer + subject. A first sign-in binds its row
|
|
281
|
+
inside the load; a miss is never cached."""
|
|
282
|
+
from velocity.cache import memo
|
|
283
|
+
|
|
284
|
+
from velocity.aws import identity
|
|
285
|
+
from velocity.aws.handlers.context import user_cache_seconds
|
|
286
|
+
|
|
287
|
+
def load():
|
|
288
|
+
return identity.find_user(
|
|
289
|
+
tx, self.user_table, issuer, subject, email, verified
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
key = ("user-subject", self.user_table, issuer, subject)
|
|
293
|
+
if getattr(self, "permission_source", None) == "tables":
|
|
294
|
+
from velocity.aws import permissions as _grants
|
|
295
|
+
|
|
296
|
+
key = (*key, _grants.cached_version(tx))
|
|
297
|
+
user = memo(
|
|
298
|
+
key,
|
|
299
|
+
load,
|
|
300
|
+
user_cache_seconds(),
|
|
301
|
+
tables=(self.user_table,),
|
|
302
|
+
cache_none=False,
|
|
303
|
+
)
|
|
304
|
+
return dict(user) if user else None
|
|
305
|
+
|
|
248
306
|
def _cached_user(self, tx, email_address):
|
|
249
307
|
"""The user-table row for the signed-in user as a dict, through the
|
|
250
308
|
process read cache (velocity.cache) for ``UserCacheSeconds``: one
|
{velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/__init__.py
RENAMED
|
@@ -23,8 +23,9 @@ from .row_scoped import RowScopedDataService
|
|
|
23
23
|
from .audit_trail import AuditTrailMixin
|
|
24
24
|
from .job_handler import JobHandler
|
|
25
25
|
from .session_control import SessionControlMixin
|
|
26
|
-
from .
|
|
27
|
-
from .
|
|
26
|
+
from .action_names import action_names
|
|
27
|
+
from .permissions_actions import PERMISSION_ACTIONS, PermissionsMixin
|
|
28
|
+
from .tiles_actions import TILE_ACTIONS, TilesMixin
|
|
28
29
|
from .client_errors import ClientErrorReporting, AnonymousClientErrorReporting
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
@@ -37,7 +38,10 @@ __all__ = [
|
|
|
37
38
|
"JobHandler",
|
|
38
39
|
"SessionControlMixin",
|
|
39
40
|
"PermissionsMixin",
|
|
41
|
+
"PERMISSION_ACTIONS",
|
|
40
42
|
"TilesMixin",
|
|
43
|
+
"TILE_ACTIONS",
|
|
44
|
+
"action_names",
|
|
41
45
|
"ClientErrorReporting",
|
|
42
46
|
"AnonymousClientErrorReporting",
|
|
43
47
|
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The client action names a handler class serves, from its methods.
|
|
2
|
+
|
|
3
|
+
``BaseHandler.format_action_name`` turns ``"tiles-load"`` into
|
|
4
|
+
``"OnActionTilesLoad"``; this is the inverse, for a site's ``client_actions``
|
|
5
|
+
allow-list: ``client_actions = MINE | TILE_ACTIONS | PERMISSION_ACTIONS``
|
|
6
|
+
picks up every action a mixin gains with a release, instead of a list each
|
|
7
|
+
site copies by hand (and forgets to extend: admin refused every tile action
|
|
8
|
+
until its list was edited, 2026-10-14).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
_PREFIX = "OnAction"
|
|
16
|
+
_WORD = re.compile(r"[A-Z][a-z0-9]*")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def action_names(cls) -> frozenset:
|
|
20
|
+
"""The kebab-case action of every ``OnAction<Name>`` method ``cls``
|
|
21
|
+
itself defines (not inherited ones; not ``OnActionDefault``)."""
|
|
22
|
+
names = set()
|
|
23
|
+
for attr, value in vars(cls).items():
|
|
24
|
+
if not attr.startswith(_PREFIX) or not callable(value):
|
|
25
|
+
continue
|
|
26
|
+
rest = attr[len(_PREFIX) :]
|
|
27
|
+
if not rest or rest == "Default":
|
|
28
|
+
continue
|
|
29
|
+
names.add("-".join(word.lower() for word in _WORD.findall(rest)))
|
|
30
|
+
return frozenset(names)
|
{velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -426,6 +426,19 @@ def plan_row_number(params):
|
|
|
426
426
|
|
|
427
427
|
# result_formats query answers with a file download (velocity.misc.export).
|
|
428
428
|
FILE_EXPORT_FORMATS = frozenset(export.EXPORT_FORMATS)
|
|
429
|
+
# Formats written as XLSX cells, which carry no time zone: aware values are
|
|
430
|
+
# converted to the handler's display_timezone() first.
|
|
431
|
+
XLSX_EXPORT_FORMATS = frozenset({"excel", "xlsx"})
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _export_zone(handler, result_format, context):
|
|
435
|
+
"""The display zone for an export: the handler's ``display_timezone``
|
|
436
|
+
for the XLSX formats, ``None`` for the text formats (which write values
|
|
437
|
+
as given and need no SSM read)."""
|
|
438
|
+
if result_format not in XLSX_EXPORT_FORMATS:
|
|
439
|
+
return None
|
|
440
|
+
zone_for = getattr(handler, "display_timezone", None)
|
|
441
|
+
return zone_for(context) if zone_for is not None else None
|
|
429
442
|
|
|
430
443
|
|
|
431
444
|
def _positive_int(value):
|
|
@@ -679,7 +692,11 @@ class ExportStream:
|
|
|
679
692
|
"""The whole export as one file: :func:`velocity.misc.export.build_buffer`
|
|
680
693
|
over :meth:`rows` (line formats are written as the batches arrive)."""
|
|
681
694
|
built = export.build_buffer(
|
|
682
|
-
result_format,
|
|
695
|
+
result_format,
|
|
696
|
+
self.headers,
|
|
697
|
+
self.rows(),
|
|
698
|
+
filename_base,
|
|
699
|
+
display_timezone=_export_zone(self.handler, result_format, self.context),
|
|
683
700
|
)
|
|
684
701
|
if built is None:
|
|
685
702
|
raise BadRequestError(f"Unsupported export format: {result_format!r}")
|
|
@@ -838,6 +855,24 @@ class DataServiceMixin:
|
|
|
838
855
|
"timestamp": "string",
|
|
839
856
|
}
|
|
840
857
|
|
|
858
|
+
def display_timezone(self, context=None):
|
|
859
|
+
"""The zone XLSX exports show times in: Excel cells carry no zone,
|
|
860
|
+
so an aware value (every ``timestamptz``) is written as its wall time
|
|
861
|
+
here. Default: the site's ``SiteTimezone`` config key (an IANA name
|
|
862
|
+
such as ``America/Denver``, read through ``ssm_config.getenv``, which
|
|
863
|
+
caches it per process); UTC, with a warning, when it is unset or not
|
|
864
|
+
a zone. Override to choose per request (``context`` is the request
|
|
865
|
+
context) or per handler. Returns a ``tzinfo``."""
|
|
866
|
+
try:
|
|
867
|
+
name = ssm_config.getenv("SiteTimezone", None)
|
|
868
|
+
except Exception:
|
|
869
|
+
logger.warning("SiteTimezone could not be read; exports use UTC")
|
|
870
|
+
name = None
|
|
871
|
+
if not name:
|
|
872
|
+
logger.warning("SiteTimezone is not set; exports show times in UTC")
|
|
873
|
+
return datetime.timezone.utc
|
|
874
|
+
return export.resolve_display_timezone(name)
|
|
875
|
+
|
|
841
876
|
def _resolve_query_max_rows(self):
|
|
842
877
|
"""Effective query row ceiling: `QueryMaxRows` config key first, then
|
|
843
878
|
the class attribute. Returns a positive int or None (no cap)."""
|
|
@@ -2231,6 +2266,7 @@ class DataServiceMixin:
|
|
|
2231
2266
|
data.get("headers", []),
|
|
2232
2267
|
data.get("rows", []),
|
|
2233
2268
|
filename_base,
|
|
2269
|
+
display_timezone=_export_zone(self, result_format, context),
|
|
2234
2270
|
)
|
|
2235
2271
|
if result_format == "excel":
|
|
2236
2272
|
# the legacy excel download keeps its caller-chosen filename
|
|
@@ -2412,7 +2448,12 @@ class DataServiceMixin:
|
|
|
2412
2448
|
)
|
|
2413
2449
|
rows = result.as_list().all()
|
|
2414
2450
|
filebuffer = BytesIO()
|
|
2415
|
-
export.create_spreadsheet(
|
|
2451
|
+
export.create_spreadsheet(
|
|
2452
|
+
headers,
|
|
2453
|
+
rows,
|
|
2454
|
+
filebuffer,
|
|
2455
|
+
display_timezone=self.display_timezone(context),
|
|
2456
|
+
)
|
|
2416
2457
|
context.response().file_download(
|
|
2417
2458
|
{
|
|
2418
2459
|
"filename": payload.get("filename", "temp_file.xls"),
|
|
@@ -63,6 +63,7 @@ from types import SimpleNamespace
|
|
|
63
63
|
|
|
64
64
|
from velocity.aws import authz, permissions
|
|
65
65
|
from velocity.aws.handlers.exceptions import AlertError
|
|
66
|
+
from .action_names import action_names
|
|
66
67
|
|
|
67
68
|
READ = "permissions.read"
|
|
68
69
|
READ_ACTION = "permissions.read"
|
|
@@ -804,3 +805,8 @@ class PermissionsMixin:
|
|
|
804
805
|
)
|
|
805
806
|
)
|
|
806
807
|
context.response().set_body(self._inbox_body(tx, actor))
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# every action PermissionsMixin serves, for a site's ``client_actions``:
|
|
811
|
+
# ``client_actions = MINE | PERMISSION_ACTIONS``
|
|
812
|
+
PERMISSION_ACTIONS = action_names(PermissionsMixin)
|
{velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/tiles_actions.py
RENAMED
|
@@ -39,6 +39,7 @@ from velocity.aws import authz, facets, permissions, tiles
|
|
|
39
39
|
from velocity.aws.handlers.exceptions import AccessError, AlertError
|
|
40
40
|
from velocity.db.fragments import Expr
|
|
41
41
|
from velocity.db.servers.tablehelper import TableHelper
|
|
42
|
+
from .action_names import action_names
|
|
42
43
|
|
|
43
44
|
logger = logging.getLogger(__name__)
|
|
44
45
|
|
|
@@ -659,3 +660,8 @@ class TilesMixin:
|
|
|
659
660
|
)
|
|
660
661
|
)
|
|
661
662
|
context.response().set_body(self._manage_body(tx, actor))
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# every action TilesMixin serves, for a site's ``client_actions``:
|
|
666
|
+
# ``client_actions = MINE | TILE_ACTIONS``
|
|
667
|
+
TILE_ACTIONS = action_names(TilesMixin)
|
{velocity_python-0.2.3 → velocity_python-0.2.4}/src/velocity/aws/handlers/mixins/web_handler.py
RENAMED
|
@@ -621,6 +621,14 @@ Request Details:
|
|
|
621
621
|
return
|
|
622
622
|
session = context.session() or {}
|
|
623
623
|
email = session.get("email_address")
|
|
624
|
+
if not email:
|
|
625
|
+
# Nobody is signed in (a public page that saves its layout as it
|
|
626
|
+
# goes): there is no owner, so nothing is saved. Rows mode used to
|
|
627
|
+
# upsert under a blank userid — one shared junk row per key that
|
|
628
|
+
# every anonymous visitor overwrote. Not an error, so a public
|
|
629
|
+
# page does not report a failure on every change.
|
|
630
|
+
context.response().set_body({"saved": False, "reason": "signed_out"})
|
|
631
|
+
return
|
|
624
632
|
if self.user_data_mode == USER_DATA_DOCUMENT:
|
|
625
633
|
owner = (getattr(self, "current_user", None) or {}).get("sys_id")
|
|
626
634
|
decision = authz.decide(
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Who is signed in: the user row bound to the identity provider's issuer and
|
|
2
|
+
subject, never found by email.
|
|
3
|
+
|
|
4
|
+
An email address can change, be reused, or be claimed by an external identity
|
|
5
|
+
provider; the provider's issuer plus subject (``iss`` + ``sub``) is the one
|
|
6
|
+
pair it guarantees never to reassign (OpenID Connect Core 1.0, section 5.7).
|
|
7
|
+
So a user row carries the pair it is bound to, and each request finds its
|
|
8
|
+
user by that pair.
|
|
9
|
+
|
|
10
|
+
The mode is the ``IdentityMode`` config key (SSM ``/{Project}/{env}/`` then
|
|
11
|
+
``/Global/{env}/``) when set, else the handler's ``identity_mode``
|
|
12
|
+
(see ``LambdaHandler.beforeAction``), else ``"email"`` — so a site switches
|
|
13
|
+
one environment at a time, demo first, with no code change:
|
|
14
|
+
|
|
15
|
+
* ``"email"`` — the default and the behaviour before this module: the user
|
|
16
|
+
row is found by ``email_address``.
|
|
17
|
+
* ``"subject"`` — the user row is found by :data:`ISSUER_COLUMN` +
|
|
18
|
+
:data:`SUBJECT_COLUMN`. The first sign-in after a site switches binds its
|
|
19
|
+
existing row ONCE, by the caller's *verified* email, and only a row not
|
|
20
|
+
yet bound to anyone; from then on email is never used to find that user.
|
|
21
|
+
A row already bound to another identity is never re-bound by email (the
|
|
22
|
+
address moved to a different account), and an unverified email binds
|
|
23
|
+
nothing.
|
|
24
|
+
|
|
25
|
+
Both columns are TEXT on the site's user table. Declare them in the site's
|
|
26
|
+
schema lane before switching a locked environment
|
|
27
|
+
(``SCHEMA_COLUMNS``); in an unlocked one the first binding creates them.
|
|
28
|
+
|
|
29
|
+
A masquerade session keeps the email path: its identity is the verified
|
|
30
|
+
grant's user, not a provider subject.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
|
|
37
|
+
from velocity.aws import ssm_config
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
IDENTITY_EMAIL = "email"
|
|
42
|
+
IDENTITY_SUBJECT = "subject"
|
|
43
|
+
IDENTITY_MODES = (IDENTITY_EMAIL, IDENTITY_SUBJECT)
|
|
44
|
+
|
|
45
|
+
ISSUER_COLUMN = "identity_issuer"
|
|
46
|
+
SUBJECT_COLUMN = "identity_subject"
|
|
47
|
+
# for a site's schema lane: ensure_columns(tx, user_table, SCHEMA_COLUMNS)
|
|
48
|
+
SCHEMA_COLUMNS = {ISSUER_COLUMN: "TEXT", SUBJECT_COLUMN: "TEXT"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mode_of(handler):
|
|
52
|
+
"""``IdentityMode`` config first, then the handler's ``identity_mode``,
|
|
53
|
+
then ``"email"``. A value that is not a mode is ``"email"`` (logged):
|
|
54
|
+
unlike authorization, the safe fallback is the established lookup, so a
|
|
55
|
+
typo never locks every user out."""
|
|
56
|
+
raw = None
|
|
57
|
+
try:
|
|
58
|
+
raw = ssm_config.getenv("IdentityMode", None)
|
|
59
|
+
except Exception:
|
|
60
|
+
raw = None
|
|
61
|
+
raw = str(raw or getattr(handler, "identity_mode", None) or IDENTITY_EMAIL)
|
|
62
|
+
mode = raw.strip().lower()
|
|
63
|
+
if mode not in IDENTITY_MODES:
|
|
64
|
+
logger.warning("identity: unknown IdentityMode %r; using email", raw)
|
|
65
|
+
return IDENTITY_EMAIL
|
|
66
|
+
return mode
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def claims_of(cognito_user):
|
|
70
|
+
"""``(issuer, subject, email, email_verified)`` from a resolved Cognito
|
|
71
|
+
user (``Context.get_cognito_user``); ``None`` parts when absent."""
|
|
72
|
+
user = cognito_user if isinstance(cognito_user, dict) else {}
|
|
73
|
+
attributes = user.get("attributes") or {}
|
|
74
|
+
email = (attributes.get("email") or user.get("email") or "").strip().lower() or None
|
|
75
|
+
verified = str(attributes.get("email_verified", "")).strip().lower() == "true"
|
|
76
|
+
return user.get("issuer") or None, user.get("sub") or None, email, verified
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _row(found):
|
|
80
|
+
if not found:
|
|
81
|
+
return None
|
|
82
|
+
return found.to_dict() if hasattr(found, "to_dict") else dict(found)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_bound(tx, user_table, issuer, subject):
|
|
86
|
+
"""The user row bound to ``issuer`` + ``subject``, or None."""
|
|
87
|
+
if not (issuer and subject):
|
|
88
|
+
return None
|
|
89
|
+
return _row(
|
|
90
|
+
tx.table(user_table).find({ISSUER_COLUMN: issuer, SUBJECT_COLUMN: subject})
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bind_by_verified_email(tx, user_table, issuer, subject, email, verified):
|
|
95
|
+
"""Bind the unbound row with this verified email to ``issuer`` +
|
|
96
|
+
``subject`` and return it; None when there is nothing it may bind.
|
|
97
|
+
|
|
98
|
+
Refuses (returns None, logs why) when the email is not verified, when no
|
|
99
|
+
row has it, or when that row is already bound to a different identity —
|
|
100
|
+
the address now belongs to another account, and following it would hand
|
|
101
|
+
that account someone else's row.
|
|
102
|
+
"""
|
|
103
|
+
if not (issuer and subject and email):
|
|
104
|
+
return None
|
|
105
|
+
if not verified:
|
|
106
|
+
logger.warning("identity: not binding %s — email not verified", email)
|
|
107
|
+
return None
|
|
108
|
+
row = _row(tx.table(user_table).find({"email_address": email}))
|
|
109
|
+
if not row:
|
|
110
|
+
return None
|
|
111
|
+
bound = (row.get(ISSUER_COLUMN), row.get(SUBJECT_COLUMN))
|
|
112
|
+
if any(bound):
|
|
113
|
+
if bound != (issuer, subject):
|
|
114
|
+
logger.warning(
|
|
115
|
+
"identity: %s row %s is bound to another identity; not re-binding by email",
|
|
116
|
+
user_table,
|
|
117
|
+
row.get("sys_id"),
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
return row
|
|
121
|
+
tx.table(user_table).update(
|
|
122
|
+
{ISSUER_COLUMN: issuer, SUBJECT_COLUMN: subject}, {"sys_id": row["sys_id"]}
|
|
123
|
+
)
|
|
124
|
+
logger.info(
|
|
125
|
+
"identity: bound %s row %s to its sign-in (first sign-in by subject)",
|
|
126
|
+
user_table,
|
|
127
|
+
row.get("sys_id"),
|
|
128
|
+
)
|
|
129
|
+
row.update({ISSUER_COLUMN: issuer, SUBJECT_COLUMN: subject})
|
|
130
|
+
return row
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_user(tx, user_table, issuer, subject, email=None, verified=False):
|
|
134
|
+
"""The signed-in caller's user row: by ``issuer`` + ``subject``, else (on
|
|
135
|
+
the first sign-in after the switch) the row its verified email binds."""
|
|
136
|
+
return find_bound(tx, user_table, issuer, subject) or bind_by_verified_email(
|
|
137
|
+
tx, user_table, issuer, subject, email, verified
|
|
138
|
+
)
|
|
@@ -13,6 +13,8 @@ holds. This module is where those come from once a site keeps them as data
|
|
|
13
13
|
``sys_permission`` the catalog (:func:`velocity.aws.authz.catalog`),
|
|
14
14
|
materialised so it is a table like any other
|
|
15
15
|
``sys_authz_state`` one counter, bumped by every change
|
|
16
|
+
``sys_permission_notice`` when each person was last told about waiting
|
|
17
|
+
requests (:func:`waiting_notices`)
|
|
16
18
|
|
|
17
19
|
**The rule.** Start with nothing. Add every *allow* from the user's current
|
|
18
20
|
roles and direct grants. Remove everything matched by any *deny* — a deny
|
|
@@ -75,6 +77,7 @@ USER_ROLE_TABLE = "sys_user_role"
|
|
|
75
77
|
USER_PERMISSION_TABLE = "sys_user_permission"
|
|
76
78
|
REQUEST_TABLE = "sys_permission_request"
|
|
77
79
|
STATE_TABLE = "sys_authz_state"
|
|
80
|
+
NOTICE_TABLE = "sys_permission_notice"
|
|
78
81
|
|
|
79
82
|
# The column on the site's user table that carries the compiled set.
|
|
80
83
|
USER_COLUMN = "permissions"
|
|
@@ -188,6 +191,23 @@ SCHEMA = {
|
|
|
188
191
|
{"columns": ["status"], "name": "idx__sys_permission_request__status"},
|
|
189
192
|
],
|
|
190
193
|
},
|
|
194
|
+
# when a person was last told about something (waiting_notices): one row
|
|
195
|
+
# per person and kind of notice, so "at most once a day" survives restarts
|
|
196
|
+
NOTICE_TABLE: {
|
|
197
|
+
"columns": {
|
|
198
|
+
"user_sys_id": "BIGINT",
|
|
199
|
+
"notice": "TEXT",
|
|
200
|
+
"sent_at": "TIMESTAMPTZ",
|
|
201
|
+
"sent_to": "TEXT",
|
|
202
|
+
"waiting": "INTEGER",
|
|
203
|
+
},
|
|
204
|
+
"unique_indexes": [
|
|
205
|
+
{
|
|
206
|
+
"columns": ["user_sys_id", "notice"],
|
|
207
|
+
"name": "idx__sys_permission_notice__user_notice",
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
},
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
GRANT_TABLES = (
|
|
@@ -203,7 +223,7 @@ GRANT_TABLES = (
|
|
|
203
223
|
# (``DataServiceMixin``): a row written around the guards would be a grant
|
|
204
224
|
# nobody was allowed to give.
|
|
205
225
|
PROTECTED_TABLES = frozenset(
|
|
206
|
-
{STATE_TABLE, PERMISSION_TABLE, REQUEST_TABLE, *GRANT_TABLES}
|
|
226
|
+
{STATE_TABLE, PERMISSION_TABLE, REQUEST_TABLE, NOTICE_TABLE, *GRANT_TABLES}
|
|
207
227
|
)
|
|
208
228
|
authz.protect_tables(*PROTECTED_TABLES)
|
|
209
229
|
|
|
@@ -1801,3 +1821,171 @@ def requests(tx, *, status=None, user_sys_id=None) -> list:
|
|
|
1801
1821
|
table = tx.table(REQUEST_TABLE)
|
|
1802
1822
|
rows = _rows(table.select(where=where) if where else table.select())
|
|
1803
1823
|
return sorted(rows, key=lambda row: row["sys_id"], reverse=True)
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
# ── telling the people who decide ──────────────────────────────────────────
|
|
1827
|
+
|
|
1828
|
+
# the kind of notice (sys_permission_notice.notice) for requests waiting
|
|
1829
|
+
REQUESTS_WAITING = "requests_waiting"
|
|
1830
|
+
# nobody is told about waiting requests more often than this
|
|
1831
|
+
NOTICE_INTERVAL = datetime.timedelta(days=1)
|
|
1832
|
+
DEFAULT_LABEL_COLUMNS = ("full_name", "email_address")
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
@dataclass(frozen=True)
|
|
1836
|
+
class WaitingNotice:
|
|
1837
|
+
"""One person to tell that requests wait for them (:func:`waiting_notices`).
|
|
1838
|
+
|
|
1839
|
+
``requests`` holds, oldest first, only what a notice may say: who asked
|
|
1840
|
+
(``person``), what for (``asked_for``: the role's name or the
|
|
1841
|
+
permission's label), where (``scope_type`` / ``scope_id``), and when
|
|
1842
|
+
(``requested_at``). Never the reason."""
|
|
1843
|
+
|
|
1844
|
+
user_sys_id: Any
|
|
1845
|
+
email: str
|
|
1846
|
+
name: str
|
|
1847
|
+
count: int
|
|
1848
|
+
oldest_at: Optional[datetime.datetime]
|
|
1849
|
+
requests: tuple
|
|
1850
|
+
notice: str = REQUESTS_WAITING
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _person_label(row, columns):
|
|
1854
|
+
for column in columns:
|
|
1855
|
+
value = str((row or {}).get(column) or "").strip()
|
|
1856
|
+
if value:
|
|
1857
|
+
return value
|
|
1858
|
+
return f"User {(row or {}).get('sys_id')}"
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def _principal_of_row(row, principal_permissions=None):
|
|
1862
|
+
"""The principal a request by this user would have (the same rule as
|
|
1863
|
+
:func:`velocity.aws.authz.principal_for`), never in a masquerade: a
|
|
1864
|
+
notice goes to the person themselves."""
|
|
1865
|
+
from types import SimpleNamespace
|
|
1866
|
+
|
|
1867
|
+
return authz.principal_for(
|
|
1868
|
+
SimpleNamespace(
|
|
1869
|
+
current_user=row,
|
|
1870
|
+
principal_permissions=principal_permissions,
|
|
1871
|
+
permission_source=TABLES,
|
|
1872
|
+
context=None,
|
|
1873
|
+
)
|
|
1874
|
+
)
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
def waiting_notices(
|
|
1878
|
+
tx,
|
|
1879
|
+
user_table,
|
|
1880
|
+
*,
|
|
1881
|
+
email_column="email_address",
|
|
1882
|
+
label_columns=DEFAULT_LABEL_COLUMNS,
|
|
1883
|
+
include=None,
|
|
1884
|
+
principal_permissions=None,
|
|
1885
|
+
every=NOTICE_INTERVAL,
|
|
1886
|
+
now=None,
|
|
1887
|
+
) -> list:
|
|
1888
|
+
"""Who must be told that access requests are waiting, and what: one
|
|
1889
|
+
:class:`WaitingNotice` per person due one. Reads only; record each
|
|
1890
|
+
notice sent with :func:`record_notice`.
|
|
1891
|
+
|
|
1892
|
+
A person is told when they hold :data:`GRANT` the way deciding needs it
|
|
1893
|
+
(site-wide, or as a super user), at least one pending request is theirs
|
|
1894
|
+
to decide (not their own, a super user excepted, as
|
|
1895
|
+
:func:`decide_request` has it), they have an address in
|
|
1896
|
+
``email_column``, ``include(row)`` (when given) says yes, and they were
|
|
1897
|
+
not told within ``every``. ``principal_permissions`` is the handler hook
|
|
1898
|
+
of the same name, for permissions a site's code hands out.
|
|
1899
|
+
|
|
1900
|
+
Nothing when the tables are not turned on or nothing waits. Nothing,
|
|
1901
|
+
with a warning, when :data:`NOTICE_TABLE` is missing: without it nobody
|
|
1902
|
+
could be told only once."""
|
|
1903
|
+
if not tables_present(tx):
|
|
1904
|
+
return []
|
|
1905
|
+
# oldest first
|
|
1906
|
+
pending = list(reversed(requests(tx, status=PENDING)))
|
|
1907
|
+
if not pending:
|
|
1908
|
+
return []
|
|
1909
|
+
notices = tx.table(NOTICE_TABLE)
|
|
1910
|
+
if not notices.exists():
|
|
1911
|
+
logger.warning(
|
|
1912
|
+
"permissions: %s is missing; nobody is told about waiting requests "
|
|
1913
|
+
"until the schema step creates it",
|
|
1914
|
+
NOTICE_TABLE,
|
|
1915
|
+
)
|
|
1916
|
+
return []
|
|
1917
|
+
now = _now(now)
|
|
1918
|
+
last = {
|
|
1919
|
+
row.get("user_sys_id"): _aware(row.get("sent_at"))
|
|
1920
|
+
for row in _rows(notices.select(where={"notice": REQUESTS_WAITING}))
|
|
1921
|
+
}
|
|
1922
|
+
people = {row.get("sys_id"): row for row in _rows(tx.table(user_table).select())}
|
|
1923
|
+
roles = {
|
|
1924
|
+
row["sys_id"]: row.get("name") for row in _rows(tx.table(ROLE_TABLE).select())
|
|
1925
|
+
}
|
|
1926
|
+
described = [
|
|
1927
|
+
{
|
|
1928
|
+
"sys_id": row["sys_id"],
|
|
1929
|
+
"user_sys_id": row.get("user_sys_id"),
|
|
1930
|
+
"person": _person_label(
|
|
1931
|
+
people.get(row.get("user_sys_id"))
|
|
1932
|
+
or {"sys_id": row.get("user_sys_id")},
|
|
1933
|
+
label_columns,
|
|
1934
|
+
),
|
|
1935
|
+
"asked_for": (
|
|
1936
|
+
(roles.get(row.get("role_sys_id")) or f"Role {row.get('role_sys_id')}")
|
|
1937
|
+
if row.get("role_sys_id")
|
|
1938
|
+
else _label(row.get("permission"))
|
|
1939
|
+
),
|
|
1940
|
+
"scope_type": row.get("scope_type"),
|
|
1941
|
+
"scope_id": row.get("scope_id"),
|
|
1942
|
+
"requested_at": _aware(row.get("requested_at")),
|
|
1943
|
+
}
|
|
1944
|
+
for row in pending
|
|
1945
|
+
]
|
|
1946
|
+
due = []
|
|
1947
|
+
for user_sys_id, row in sorted(people.items(), key=lambda item: str(item[0])):
|
|
1948
|
+
principal = _principal_of_row(row, principal_permissions)
|
|
1949
|
+
if principal.user_sys_id is None or not principal.holds(GRANT):
|
|
1950
|
+
continue
|
|
1951
|
+
email = str(row.get(email_column) or "").strip()
|
|
1952
|
+
if not email or (include is not None and not include(row)):
|
|
1953
|
+
continue
|
|
1954
|
+
sent_at = last.get(principal.user_sys_id)
|
|
1955
|
+
if sent_at is not None and sent_at > now - every:
|
|
1956
|
+
continue
|
|
1957
|
+
theirs = tuple(
|
|
1958
|
+
entry
|
|
1959
|
+
for entry in described
|
|
1960
|
+
if principal.superuser or entry["user_sys_id"] != principal.user_sys_id
|
|
1961
|
+
)
|
|
1962
|
+
if not theirs:
|
|
1963
|
+
continue
|
|
1964
|
+
times = [e["requested_at"] for e in theirs if e["requested_at"] is not None]
|
|
1965
|
+
due.append(
|
|
1966
|
+
WaitingNotice(
|
|
1967
|
+
user_sys_id=principal.user_sys_id,
|
|
1968
|
+
email=email,
|
|
1969
|
+
name=_person_label(row, label_columns),
|
|
1970
|
+
count=len(theirs),
|
|
1971
|
+
oldest_at=min(times) if times else None,
|
|
1972
|
+
requests=theirs,
|
|
1973
|
+
)
|
|
1974
|
+
)
|
|
1975
|
+
return due
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
def record_notice(tx, notice, *, now=None):
|
|
1979
|
+
"""Record that ``notice`` (a :class:`WaitingNotice`) was sent, so the
|
|
1980
|
+
person is not told again within the interval. Call it in the
|
|
1981
|
+
transaction, in the same savepoint as the send."""
|
|
1982
|
+
tx.table(NOTICE_TABLE).merge(
|
|
1983
|
+
{
|
|
1984
|
+
"user_sys_id": notice.user_sys_id,
|
|
1985
|
+
"notice": notice.notice,
|
|
1986
|
+
"sent_at": _now(now),
|
|
1987
|
+
"sent_to": notice.email,
|
|
1988
|
+
"waiting": notice.count,
|
|
1989
|
+
},
|
|
1990
|
+
pk={"user_sys_id": notice.user_sys_id, "notice": notice.notice},
|
|
1991
|
+
)
|