velocity-python 0.1.62__tar.gz → 0.1.66__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.62 → velocity_python-0.1.66}/PKG-INFO +1 -1
- {velocity_python-0.1.62 → velocity_python-0.1.66}/pyproject.toml +1 -1
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/context.py +37 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/lambda_handler.py +34 -1
- velocity_python-0.1.66/src/velocity/aws/handlers/masquerade.py +150 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/web_handler.py +9 -4
- velocity_python-0.1.66/src/velocity/db/core/jsonproxy.py +175 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/row.py +19 -3
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/table.py +24 -7
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/base/sql.py +16 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/types.py +7 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/sql.py +14 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/types.py +10 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/sql.py +3 -3
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/SOURCES.txt +5 -0
- velocity_python-0.1.66/tests/test_json_columns.py +305 -0
- velocity_python-0.1.66/tests/test_lambda_handler_masquerade.py +158 -0
- velocity_python-0.1.66/tests/test_masquerade_grant.py +111 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/LICENSE +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/README.md +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/setup.cfg +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_observability.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.62 → velocity_python-0.1.66}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -167,6 +167,43 @@ class Context:
|
|
|
167
167
|
self.perf.set_enabled(enabled)
|
|
168
168
|
return enabled
|
|
169
169
|
|
|
170
|
+
# Header (case-insensitive) carrying a signed masquerade grant minted by an
|
|
171
|
+
# authorized administrative service. See velocity.aws.handlers.masquerade.
|
|
172
|
+
MASQUERADE_HEADER = "x-cc-masquerade"
|
|
173
|
+
|
|
174
|
+
def get_masquerade_grant(self):
|
|
175
|
+
"""Return the verified masquerade grant payload for this request, or None.
|
|
176
|
+
|
|
177
|
+
The grant is read from the ``x-cc-masquerade`` header and verified
|
|
178
|
+
against the ``MasqueradeSigningKey`` config value. Returns ``None`` when
|
|
179
|
+
the feature is off (no key), no header is present, or the grant is
|
|
180
|
+
invalid/expired — callers then fall back to normal identity. A present
|
|
181
|
+
but invalid grant is logged and treated as absent (fail closed).
|
|
182
|
+
"""
|
|
183
|
+
headers = self.__aws_event.get("headers") or {}
|
|
184
|
+
token = None
|
|
185
|
+
for name, value in headers.items():
|
|
186
|
+
if isinstance(name, str) and name.lower() == self.MASQUERADE_HEADER:
|
|
187
|
+
token = value
|
|
188
|
+
break
|
|
189
|
+
if not token:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
secret = config_getenv("MasqueradeSigningKey", "") or ""
|
|
193
|
+
if not secret:
|
|
194
|
+
logger.warning(
|
|
195
|
+
"Masquerade grant header present but MasqueradeSigningKey is not configured"
|
|
196
|
+
)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
from velocity.aws.handlers.masquerade import MasqueradeError, verify_grant
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
return verify_grant(secret, token)
|
|
203
|
+
except MasqueradeError as exc:
|
|
204
|
+
logger.warning("Rejected masquerade grant: %s", exc)
|
|
205
|
+
return None
|
|
206
|
+
|
|
170
207
|
def _build_session(self, aws_event):
|
|
171
208
|
request_context = aws_event.get("requestContext") or {}
|
|
172
209
|
identity = request_context.get("identity") or {}
|
{velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/lambda_handler.py
RENAMED
|
@@ -69,7 +69,40 @@ class LambdaHandler(BaseHandler):
|
|
|
69
69
|
auth_mode = "none"
|
|
70
70
|
require_db_user = False
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
# Masquerade: a signed grant lets an authorized admin act as another
|
|
73
|
+
# user. When a valid grant scoped to this app's user_table is present,
|
|
74
|
+
# the request identity resolves to the target (effective_user) exactly
|
|
75
|
+
# as if they had signed in, while the real admin is retained for audit.
|
|
76
|
+
# The grant signature is verified in context.get_masquerade_grant().
|
|
77
|
+
masquerade = None
|
|
78
|
+
if (
|
|
79
|
+
auth_mode != "none"
|
|
80
|
+
and getattr(self, "allow_masquerade", True)
|
|
81
|
+
and getattr(self, "user_table", None)
|
|
82
|
+
):
|
|
83
|
+
grant = context.get_masquerade_grant()
|
|
84
|
+
if grant and grant.get("pool") == self.user_table:
|
|
85
|
+
masquerade = grant
|
|
86
|
+
session["real_user"] = grant["real_user"]
|
|
87
|
+
session["email_address"] = grant["effective_user"]
|
|
88
|
+
session["masquerade"] = {
|
|
89
|
+
"real_user": grant["real_user"],
|
|
90
|
+
"effective_user": grant["effective_user"],
|
|
91
|
+
"pool": grant["pool"],
|
|
92
|
+
"jti": grant.get("jti"),
|
|
93
|
+
"exp": grant.get("exp"),
|
|
94
|
+
}
|
|
95
|
+
logger.info(
|
|
96
|
+
"Masquerade active: %s acting as %s on %s",
|
|
97
|
+
grant["real_user"],
|
|
98
|
+
grant["effective_user"],
|
|
99
|
+
grant["pool"],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if masquerade or auth_mode == "none":
|
|
103
|
+
# In masquerade mode the caller's own Cognito token is for a
|
|
104
|
+
# different pool (e.g. the admin pool), so skip the pool-scoped
|
|
105
|
+
# Cognito lookup; identity comes from the verified grant instead.
|
|
73
106
|
self.cognito_user = None
|
|
74
107
|
else:
|
|
75
108
|
context.perf.start("get_cognito_user")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Signed masquerade (impersonation) grants.
|
|
2
|
+
|
|
3
|
+
A masquerade grant is a compact, HMAC-SHA256 signed token that authorizes one
|
|
4
|
+
identity (``real_user``) to act as another (``effective_user``) within a named
|
|
5
|
+
scope (``pool``) for a short window. It is the trust vehicle for admin
|
|
6
|
+
"masquerade as user" sessions: an administrative service mints a grant after
|
|
7
|
+
authorizing the operation, and the target application verifies the grant's
|
|
8
|
+
signature before resolving the request's identity to ``effective_user`` while
|
|
9
|
+
retaining ``real_user`` for audit.
|
|
10
|
+
|
|
11
|
+
The format is intentionally small and dependency-free (no JWT library):
|
|
12
|
+
|
|
13
|
+
base64url(payload_json) + "." + base64url(hmac_sha256(secret, body))
|
|
14
|
+
|
|
15
|
+
Payload claims:
|
|
16
|
+
|
|
17
|
+
real_user identity performing the masquerade (e.g. admin email)
|
|
18
|
+
effective_user identity being acted as (e.g. donor/client email)
|
|
19
|
+
pool scope the grant is valid for (e.g. "client_users")
|
|
20
|
+
iat issued-at unix seconds
|
|
21
|
+
exp expiry unix seconds
|
|
22
|
+
jti unique id for one-time-use / revocation tracking
|
|
23
|
+
|
|
24
|
+
This module is business-agnostic: it knows nothing about CaringCent pools,
|
|
25
|
+
Cognito, or specific apps. Callers supply the secret and claim values.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import hashlib
|
|
30
|
+
import hmac
|
|
31
|
+
import json
|
|
32
|
+
import secrets as _secrets
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"MasqueradeError",
|
|
37
|
+
"mint_grant",
|
|
38
|
+
"verify_grant",
|
|
39
|
+
"DEFAULT_TTL_SECONDS",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
DEFAULT_TTL_SECONDS = 900 # 15 minutes
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MasqueradeError(Exception):
|
|
46
|
+
"""Raised when a masquerade grant is malformed, unsigned, or expired."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _b64u_encode(raw: bytes) -> str:
|
|
50
|
+
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _b64u_decode(value: str) -> bytes:
|
|
54
|
+
padding = "=" * (-len(value) % 4)
|
|
55
|
+
return base64.urlsafe_b64decode(value + padding)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _secret_bytes(secret) -> bytes:
|
|
59
|
+
if secret is None:
|
|
60
|
+
raise MasqueradeError("Masquerade signing secret is not configured")
|
|
61
|
+
if isinstance(secret, bytes):
|
|
62
|
+
secret_bytes = secret
|
|
63
|
+
else:
|
|
64
|
+
secret_bytes = str(secret).encode("utf-8")
|
|
65
|
+
if not secret_bytes:
|
|
66
|
+
raise MasqueradeError("Masquerade signing secret is empty")
|
|
67
|
+
return secret_bytes
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sign(secret_bytes: bytes, body: str) -> str:
|
|
71
|
+
digest = hmac.new(secret_bytes, body.encode("ascii"), hashlib.sha256).digest()
|
|
72
|
+
return _b64u_encode(digest)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mint_grant(
|
|
76
|
+
secret,
|
|
77
|
+
*,
|
|
78
|
+
real_user: str,
|
|
79
|
+
effective_user: str,
|
|
80
|
+
pool: str,
|
|
81
|
+
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
|
82
|
+
jti: str = None,
|
|
83
|
+
now: int = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Create a signed masquerade grant string.
|
|
86
|
+
|
|
87
|
+
``real_user``, ``effective_user`` and ``pool`` are required and must be
|
|
88
|
+
non-empty. ``ttl_seconds`` bounds validity; ``jti`` defaults to a random id.
|
|
89
|
+
"""
|
|
90
|
+
real_user = (real_user or "").strip()
|
|
91
|
+
effective_user = (effective_user or "").strip()
|
|
92
|
+
pool = (pool or "").strip()
|
|
93
|
+
if not real_user or not effective_user or not pool:
|
|
94
|
+
raise MasqueradeError("real_user, effective_user and pool are required")
|
|
95
|
+
if ttl_seconds <= 0:
|
|
96
|
+
raise MasqueradeError("ttl_seconds must be positive")
|
|
97
|
+
|
|
98
|
+
issued = int(now if now is not None else time.time())
|
|
99
|
+
payload = {
|
|
100
|
+
"real_user": real_user,
|
|
101
|
+
"effective_user": effective_user,
|
|
102
|
+
"pool": pool,
|
|
103
|
+
"iat": issued,
|
|
104
|
+
"exp": issued + int(ttl_seconds),
|
|
105
|
+
"jti": jti or _secrets.token_urlsafe(16),
|
|
106
|
+
}
|
|
107
|
+
body = _b64u_encode(
|
|
108
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
109
|
+
)
|
|
110
|
+
signature = _sign(_secret_bytes(secret), body)
|
|
111
|
+
return f"{body}.{signature}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def verify_grant(secret, token, *, now: int = None) -> dict:
|
|
115
|
+
"""Verify a grant's signature and expiry, returning its payload.
|
|
116
|
+
|
|
117
|
+
Raises :class:`MasqueradeError` if the token is malformed, the signature
|
|
118
|
+
does not match, the payload is structurally invalid, or it has expired.
|
|
119
|
+
"""
|
|
120
|
+
if not token or not isinstance(token, str) or "." not in token:
|
|
121
|
+
raise MasqueradeError("Malformed masquerade grant")
|
|
122
|
+
|
|
123
|
+
body, _, signature = token.partition(".")
|
|
124
|
+
expected = _sign(_secret_bytes(secret), body)
|
|
125
|
+
# Constant-time comparison to avoid signature timing oracles.
|
|
126
|
+
if not hmac.compare_digest(signature, expected):
|
|
127
|
+
raise MasqueradeError("Masquerade grant signature mismatch")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
payload = json.loads(_b64u_decode(body).decode("utf-8"))
|
|
131
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
132
|
+
raise MasqueradeError("Masquerade grant payload is not valid JSON") from exc
|
|
133
|
+
|
|
134
|
+
if not isinstance(payload, dict):
|
|
135
|
+
raise MasqueradeError("Masquerade grant payload is not an object")
|
|
136
|
+
|
|
137
|
+
for field in ("real_user", "effective_user", "pool", "exp"):
|
|
138
|
+
if not payload.get(field):
|
|
139
|
+
raise MasqueradeError(f"Masquerade grant missing '{field}'")
|
|
140
|
+
|
|
141
|
+
current = int(now if now is not None else time.time())
|
|
142
|
+
try:
|
|
143
|
+
expires = int(payload["exp"])
|
|
144
|
+
except (TypeError, ValueError) as exc:
|
|
145
|
+
raise MasqueradeError("Masquerade grant 'exp' is not an integer") from exc
|
|
146
|
+
|
|
147
|
+
if current >= expires:
|
|
148
|
+
raise MasqueradeError("Masquerade grant has expired")
|
|
149
|
+
|
|
150
|
+
return payload
|
{velocity_python-0.1.62 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/web_handler.py
RENAMED
|
@@ -461,12 +461,17 @@ Request Details:
|
|
|
461
461
|
data = context.get_pass_through_vars(tx)
|
|
462
462
|
current_user = getattr(self, "current_user", None) or {}
|
|
463
463
|
context.response().load_object(current_user)
|
|
464
|
+
controls = {
|
|
465
|
+
**data.get("controls", {}),
|
|
466
|
+
"current_user": current_user,
|
|
467
|
+
}
|
|
468
|
+
# When the session is a masquerade, surface it so the UI can show the
|
|
469
|
+
# admin's real identity alongside the user they are acting as.
|
|
470
|
+
masquerade = (context.session() or {}).get("masquerade")
|
|
471
|
+
controls["masquerade"] = masquerade or None
|
|
464
472
|
context.response().update_store(
|
|
465
473
|
{
|
|
466
|
-
"controls":
|
|
467
|
-
**data.get("controls", {}),
|
|
468
|
-
"current_user": current_user,
|
|
469
|
-
},
|
|
474
|
+
"controls": controls,
|
|
470
475
|
"repo": {
|
|
471
476
|
"current_user": current_user,
|
|
472
477
|
},
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live list/dict proxies for JSON column values.
|
|
3
|
+
|
|
4
|
+
A ``list`` or ``dict`` stored in a row is just a JSON column (JSONB on
|
|
5
|
+
PostgreSQL). ``Row.__getitem__`` wraps such values in :class:`BoundList` /
|
|
6
|
+
:class:`BoundDict` so in-place mutation writes the whole value back through
|
|
7
|
+
the owning row::
|
|
8
|
+
|
|
9
|
+
row["tags"].append("vip") # UPDATE ... SET tags = '["a","vip"]'
|
|
10
|
+
row["meta"]["verified"] = True # whole "meta" value written back
|
|
11
|
+
|
|
12
|
+
There are no relation declarations and no diffing: every mutation persists
|
|
13
|
+
the complete top-level value. Nested containers share the root's flush, so
|
|
14
|
+
a deep mutation also writes the root value back.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Controller:
|
|
19
|
+
"""Shared link between a root proxy and all of its nested proxies."""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("flush", "root")
|
|
22
|
+
|
|
23
|
+
def __init__(self, flush):
|
|
24
|
+
self.flush = flush
|
|
25
|
+
self.root = None
|
|
26
|
+
|
|
27
|
+
def notify(self):
|
|
28
|
+
self.flush(unwrap(self.root))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bind(value, flush):
|
|
32
|
+
"""
|
|
33
|
+
Wrap ``value`` (a dict or list) in a bound proxy tree.
|
|
34
|
+
|
|
35
|
+
``flush(plain_value)`` is called with the plain (unwrapped) top-level
|
|
36
|
+
value after every mutation anywhere in the tree. Non-container values
|
|
37
|
+
are returned unchanged.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(value, (dict, list)):
|
|
40
|
+
return value
|
|
41
|
+
ctrl = _Controller(flush)
|
|
42
|
+
root = _wrap(value, ctrl)
|
|
43
|
+
ctrl.root = root
|
|
44
|
+
return root
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def unwrap(value):
|
|
48
|
+
"""Return a plain dict/list copy of a (possibly bound) value tree."""
|
|
49
|
+
if isinstance(value, dict):
|
|
50
|
+
return {k: unwrap(v) for k, v in value.items()}
|
|
51
|
+
if isinstance(value, list):
|
|
52
|
+
return [unwrap(v) for v in value]
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _wrap(value, ctrl):
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
return BoundDict(value, ctrl)
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return BoundList(value, ctrl)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BoundList(list):
|
|
65
|
+
"""A list whose mutations flush the root value back to its column."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, iterable, ctrl):
|
|
68
|
+
super().__init__(_wrap(v, ctrl) for v in iterable)
|
|
69
|
+
self._ctrl = ctrl
|
|
70
|
+
|
|
71
|
+
# -- mutators ------------------------------------------------------
|
|
72
|
+
def __setitem__(self, index, value):
|
|
73
|
+
if isinstance(index, slice):
|
|
74
|
+
value = [_wrap(v, self._ctrl) for v in value]
|
|
75
|
+
else:
|
|
76
|
+
value = _wrap(value, self._ctrl)
|
|
77
|
+
super().__setitem__(index, value)
|
|
78
|
+
self._ctrl.notify()
|
|
79
|
+
|
|
80
|
+
def __delitem__(self, index):
|
|
81
|
+
super().__delitem__(index)
|
|
82
|
+
self._ctrl.notify()
|
|
83
|
+
|
|
84
|
+
def __iadd__(self, other):
|
|
85
|
+
self.extend(other)
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def __imul__(self, n):
|
|
89
|
+
result = super().__imul__(n)
|
|
90
|
+
self._ctrl.notify()
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def append(self, value):
|
|
94
|
+
super().append(_wrap(value, self._ctrl))
|
|
95
|
+
self._ctrl.notify()
|
|
96
|
+
|
|
97
|
+
def extend(self, iterable):
|
|
98
|
+
super().extend(_wrap(v, self._ctrl) for v in iterable)
|
|
99
|
+
self._ctrl.notify()
|
|
100
|
+
|
|
101
|
+
def insert(self, index, value):
|
|
102
|
+
super().insert(index, _wrap(value, self._ctrl))
|
|
103
|
+
self._ctrl.notify()
|
|
104
|
+
|
|
105
|
+
def remove(self, value):
|
|
106
|
+
super().remove(value)
|
|
107
|
+
self._ctrl.notify()
|
|
108
|
+
|
|
109
|
+
def pop(self, index=-1):
|
|
110
|
+
result = super().pop(index)
|
|
111
|
+
self._ctrl.notify()
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
def clear(self):
|
|
115
|
+
super().clear()
|
|
116
|
+
self._ctrl.notify()
|
|
117
|
+
|
|
118
|
+
def sort(self, **kwds):
|
|
119
|
+
super().sort(**kwds)
|
|
120
|
+
self._ctrl.notify()
|
|
121
|
+
|
|
122
|
+
def reverse(self):
|
|
123
|
+
super().reverse()
|
|
124
|
+
self._ctrl.notify()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BoundDict(dict):
|
|
128
|
+
"""A dict whose mutations flush the root value back to its column."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, mapping, ctrl):
|
|
131
|
+
super().__init__({k: _wrap(v, ctrl) for k, v in mapping.items()})
|
|
132
|
+
self._ctrl = ctrl
|
|
133
|
+
|
|
134
|
+
# -- mutators ------------------------------------------------------
|
|
135
|
+
def __setitem__(self, key, value):
|
|
136
|
+
super().__setitem__(key, _wrap(value, self._ctrl))
|
|
137
|
+
self._ctrl.notify()
|
|
138
|
+
|
|
139
|
+
def __delitem__(self, key):
|
|
140
|
+
super().__delitem__(key)
|
|
141
|
+
self._ctrl.notify()
|
|
142
|
+
|
|
143
|
+
def update(self, dict_=None, **kwds):
|
|
144
|
+
data = {}
|
|
145
|
+
if dict_:
|
|
146
|
+
data.update(dict_)
|
|
147
|
+
data.update(kwds)
|
|
148
|
+
for k, v in data.items():
|
|
149
|
+
super().__setitem__(k, _wrap(v, self._ctrl))
|
|
150
|
+
if data:
|
|
151
|
+
self._ctrl.notify()
|
|
152
|
+
|
|
153
|
+
def pop(self, key, *args):
|
|
154
|
+
existed = key in self
|
|
155
|
+
result = super().pop(key, *args)
|
|
156
|
+
if existed:
|
|
157
|
+
self._ctrl.notify()
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def popitem(self):
|
|
161
|
+
result = super().popitem()
|
|
162
|
+
self._ctrl.notify()
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
def clear(self):
|
|
166
|
+
super().clear()
|
|
167
|
+
self._ctrl.notify()
|
|
168
|
+
|
|
169
|
+
def setdefault(self, key, default=None):
|
|
170
|
+
if key in self:
|
|
171
|
+
return self[key]
|
|
172
|
+
wrapped = _wrap(default, self._ctrl)
|
|
173
|
+
super().__setitem__(key, wrapped)
|
|
174
|
+
self._ctrl.notify()
|
|
175
|
+
return wrapped
|
|
@@ -2,6 +2,7 @@ import pprint
|
|
|
2
2
|
import time as _time
|
|
3
3
|
from collections.abc import MutableMapping
|
|
4
4
|
from velocity.db.exceptions import DbColumnMissingError
|
|
5
|
+
from velocity.db.core import jsonproxy
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
# Attributes that live on the Row instance itself and must never be
|
|
@@ -133,9 +134,24 @@ class Row(MutableMapping):
|
|
|
133
134
|
return self.pk[key]
|
|
134
135
|
cache = self._ensure_cache()
|
|
135
136
|
if key in cache:
|
|
136
|
-
|
|
137
|
+
val = cache[key]
|
|
138
|
+
# JSON columns come back as plain dict/list — wrap them in live
|
|
139
|
+
# proxies so in-place mutation writes the whole value back.
|
|
140
|
+
if isinstance(val, (dict, list)) and not isinstance(
|
|
141
|
+
val, (jsonproxy.BoundDict, jsonproxy.BoundList)
|
|
142
|
+
):
|
|
143
|
+
val = jsonproxy.bind(
|
|
144
|
+
val, lambda plain, _k=key: self.__setitem__(_k, plain)
|
|
145
|
+
)
|
|
146
|
+
cache[key] = val
|
|
147
|
+
return val
|
|
137
148
|
# Fall back to a direct DB fetch for columns not in the initial SELECT
|
|
138
|
-
|
|
149
|
+
val = self.table.get_value(key, self.pk)
|
|
150
|
+
if isinstance(val, (dict, list)):
|
|
151
|
+
val = jsonproxy.bind(
|
|
152
|
+
val, lambda plain, _k=key: self.__setitem__(_k, plain)
|
|
153
|
+
)
|
|
154
|
+
return val
|
|
139
155
|
|
|
140
156
|
def __setitem__(self, key, val):
|
|
141
157
|
if key in self.pk:
|
|
@@ -327,7 +343,7 @@ class Row(MutableMapping):
|
|
|
327
343
|
"""
|
|
328
344
|
Returns the row as a dictionary (from cache or via a SELECT on self.pk).
|
|
329
345
|
"""
|
|
330
|
-
return
|
|
346
|
+
return {k: jsonproxy.unwrap(v) for k, v in self._ensure_cache().items()}
|
|
331
347
|
|
|
332
348
|
def extract(self, *args):
|
|
333
349
|
"""
|
|
@@ -1984,12 +1984,21 @@ class Table:
|
|
|
1984
1984
|
_ddl_logger.warning("DDL ALTER COLUMN TYPE on %s column=%s", self.name, column)
|
|
1985
1985
|
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
1986
1986
|
|
|
1987
|
+
def _adapt_data(self, data):
|
|
1988
|
+
"""
|
|
1989
|
+
Apply the dialect's data-value adaptation (e.g. dict/list -> JSONB on
|
|
1990
|
+
PostgreSQL, JSON text elsewhere) to a column->value mapping. Used for
|
|
1991
|
+
INSERT/UPDATE data only — WHERE values keep native driver adaptation.
|
|
1992
|
+
"""
|
|
1993
|
+
adapt = self.sql.adapt_data_value
|
|
1994
|
+
return {k: adapt(v) for k, v in data.items()}
|
|
1995
|
+
|
|
1987
1996
|
@create_missing
|
|
1988
1997
|
def update(self, data, where=None, pk=None, **kwds):
|
|
1989
1998
|
"""
|
|
1990
1999
|
Performs an UPDATE of rows matching `where` or `pk` with `data`.
|
|
1991
2000
|
"""
|
|
1992
|
-
sql, vals = self.sql.update(self.tx, self.name, data, where, pk)
|
|
2001
|
+
sql, vals = self.sql.update(self.tx, self.name, self._adapt_data(data), where, pk)
|
|
1993
2002
|
if kwds.get("sql_only", False):
|
|
1994
2003
|
return sql, vals
|
|
1995
2004
|
self.tx.invalidate_cache(self.name)
|
|
@@ -2002,7 +2011,7 @@ class Table:
|
|
|
2002
2011
|
"""
|
|
2003
2012
|
Performs an INSERT of the given data into this table. Resets sys_id on duplicate keys if needed.
|
|
2004
2013
|
"""
|
|
2005
|
-
sql, vals = self.sql.insert(self.name, data)
|
|
2014
|
+
sql, vals = self.sql.insert(self.name, self._adapt_data(data))
|
|
2006
2015
|
if kwds.get("sql_only", False):
|
|
2007
2016
|
return sql, vals
|
|
2008
2017
|
self.tx.invalidate_cache(self.name)
|
|
@@ -2018,7 +2027,7 @@ class Table:
|
|
|
2018
2027
|
sql, vals = self.sql.merge(
|
|
2019
2028
|
self.tx,
|
|
2020
2029
|
self.name,
|
|
2021
|
-
data,
|
|
2030
|
+
self._adapt_data(data),
|
|
2022
2031
|
pk,
|
|
2023
2032
|
on_conflict_do_nothing=False,
|
|
2024
2033
|
on_conflict_update=True,
|
|
@@ -2047,7 +2056,9 @@ class Table:
|
|
|
2047
2056
|
"""
|
|
2048
2057
|
if not rows:
|
|
2049
2058
|
return 0
|
|
2050
|
-
sql, args_list, template = self.sql.insert_many(
|
|
2059
|
+
sql, args_list, template = self.sql.insert_many(
|
|
2060
|
+
self.name, [self._adapt_data(row) for row in rows]
|
|
2061
|
+
)
|
|
2051
2062
|
page_size = kwds.get("page_size", 1000)
|
|
2052
2063
|
self.tx.invalidate_cache(self.name)
|
|
2053
2064
|
return self.tx._execute_values(sql, args_list, template=template, page_size=page_size)
|
|
@@ -2068,7 +2079,9 @@ class Table:
|
|
|
2068
2079
|
"""
|
|
2069
2080
|
if not rows:
|
|
2070
2081
|
return 0
|
|
2071
|
-
sql, args_list, template = self.sql.merge_many(
|
|
2082
|
+
sql, args_list, template = self.sql.merge_many(
|
|
2083
|
+
self.tx, self.name, [self._adapt_data(row) for row in rows], pk=pk
|
|
2084
|
+
)
|
|
2072
2085
|
page_size = kwds.get("page_size", 1000)
|
|
2073
2086
|
self.tx.invalidate_cache(self.name)
|
|
2074
2087
|
return self.tx._execute_values(sql, args_list, template=template, page_size=page_size)
|
|
@@ -2172,7 +2185,9 @@ class Table:
|
|
|
2172
2185
|
"Current SQL dialect does not support insert-if-not-exists operations."
|
|
2173
2186
|
)
|
|
2174
2187
|
|
|
2175
|
-
sql, vals = ins_builder(
|
|
2188
|
+
sql, vals = ins_builder(
|
|
2189
|
+
self.tx, self.name, self._adapt_data(insert_payload), exists_where
|
|
2190
|
+
)
|
|
2176
2191
|
if sql_only:
|
|
2177
2192
|
return {"update": update_stmt, "insert": (sql, vals)}
|
|
2178
2193
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
@@ -2200,7 +2215,9 @@ class Table:
|
|
|
2200
2215
|
must be present in `data`.
|
|
2201
2216
|
:return: rowcount (0 or 1) or (sql, params) when sql_only=True
|
|
2202
2217
|
"""
|
|
2203
|
-
sql, vals = self.sql.insert_if_not_exists(
|
|
2218
|
+
sql, vals = self.sql.insert_if_not_exists(
|
|
2219
|
+
self.tx, self.name, self._adapt_data(data), where
|
|
2220
|
+
)
|
|
2204
2221
|
if kwds.get("sql_only", False):
|
|
2205
2222
|
return sql, vals
|
|
2206
2223
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Abstract base class for SQL dialect implementations.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
6
7
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
7
8
|
|
|
@@ -38,6 +39,21 @@ class BaseSQLDialect(ABC):
|
|
|
38
39
|
DatabaseObjectExistsErrorCodes: List[str] = []
|
|
39
40
|
DataIntegrityErrorCodes: List[str] = []
|
|
40
41
|
|
|
42
|
+
@classmethod
|
|
43
|
+
def adapt_data_value(cls, val: Any) -> Any:
|
|
44
|
+
"""
|
|
45
|
+
Adapt a Python value being written to a column (INSERT/UPDATE data
|
|
46
|
+
values only — never WHERE parameters, which keep their native driver
|
|
47
|
+
adaptation, e.g. list -> ANY(...) on PostgreSQL).
|
|
48
|
+
|
|
49
|
+
Default: serialize dict/list to a JSON string for backends without a
|
|
50
|
+
native JSON parameter adapter (SQLite, SQL Server, MySQL). PostgreSQL
|
|
51
|
+
overrides this to bind JSONB natively.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(val, (dict, list)):
|
|
54
|
+
return json.dumps(val)
|
|
55
|
+
return val
|
|
56
|
+
|
|
41
57
|
@classmethod
|
|
42
58
|
def quote_identifier(cls, name: str) -> str:
|
|
43
59
|
"""Always-quote a single SQL identifier to prevent injection.
|
|
@@ -23,6 +23,7 @@ class TYPES(BaseTypes):
|
|
|
23
23
|
LONGTEXT = "LONGTEXT"
|
|
24
24
|
MEDIUMTEXT = "MEDIUMTEXT"
|
|
25
25
|
VARCHAR = "VARCHAR"
|
|
26
|
+
JSON = "JSON"
|
|
26
27
|
|
|
27
28
|
@classmethod
|
|
28
29
|
def get_type(cls, v):
|
|
@@ -51,6 +52,8 @@ class TYPES(BaseTypes):
|
|
|
51
52
|
return cls.TIME
|
|
52
53
|
if isinstance(v, bytes) or v is bytes:
|
|
53
54
|
return cls.BINARY
|
|
55
|
+
if isinstance(v, (dict, list)) or v is dict or v is list:
|
|
56
|
+
return cls.JSON
|
|
54
57
|
return cls.TEXT
|
|
55
58
|
|
|
56
59
|
@classmethod
|
|
@@ -80,6 +83,8 @@ class TYPES(BaseTypes):
|
|
|
80
83
|
return cls.TIME
|
|
81
84
|
if isinstance(v, bytes) or v is bytes:
|
|
82
85
|
return cls.BINARY
|
|
86
|
+
if isinstance(v, (dict, list)) or v is dict or v is list:
|
|
87
|
+
return cls.JSON
|
|
83
88
|
return cls.TEXT
|
|
84
89
|
|
|
85
90
|
@classmethod
|
|
@@ -108,4 +113,6 @@ class TYPES(BaseTypes):
|
|
|
108
113
|
return datetime.datetime
|
|
109
114
|
if v == cls.BINARY or "BLOB" in v:
|
|
110
115
|
return bytes
|
|
116
|
+
if v == cls.JSON:
|
|
117
|
+
return dict
|
|
111
118
|
raise Exception(f"Unmapped MySQL type {v}")
|
|
@@ -71,6 +71,20 @@ class SQL(BaseSQLDialect):
|
|
|
71
71
|
DatabaseObjectExistsErrorCodes = ["42710", "42P07", "42P04"]
|
|
72
72
|
DataIntegrityErrorCodes = ["23503", "23502", "23514", "23P01", "22003"]
|
|
73
73
|
|
|
74
|
+
@classmethod
|
|
75
|
+
def adapt_data_value(cls, val):
|
|
76
|
+
"""
|
|
77
|
+
Bind dict/list data values as JSONB. Lists must be wrapped here
|
|
78
|
+
because psycopg's native list adaptation targets PostgreSQL arrays
|
|
79
|
+
(kept for WHERE params, e.g. ``= ANY(%s)``); bare dicts also work via
|
|
80
|
+
the engine-registered dumper, but wrapping both keeps it uniform.
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(val, (dict, list)):
|
|
83
|
+
from psycopg.types.json import Jsonb
|
|
84
|
+
|
|
85
|
+
return Jsonb(val)
|
|
86
|
+
return val
|
|
87
|
+
|
|
74
88
|
@classmethod
|
|
75
89
|
def get_error(cls, e):
|
|
76
90
|
# psycopg v3 uses 'sqlstate'; psycopg2 used 'pgcode'.
|