velocity-python 0.1.103__tar.gz → 0.1.105__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.103 → velocity_python-0.1.105}/PKG-INFO +1 -1
- {velocity_python-0.1.103 → velocity_python-0.1.105}/pyproject.toml +1 -1
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.105/src/velocity/aws/feature_flags.py +159 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/mixins/__init__.py +4 -0
- velocity_python-0.1.105/src/velocity/aws/handlers/mixins/session_control.py +277 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/pdf.py +14 -2
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/SOURCES.txt +4 -0
- velocity_python-0.1.105/tests/test_feature_flags.py +176 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_pdf.py +25 -0
- velocity_python-0.1.105/tests/test_session_control.py +255 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/LICENSE +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/README.md +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/setup.cfg +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_blob_store.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_observability.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_response.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.103 → velocity_python-0.1.105}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Environment-scoped feature flags with cross-database mirroring.
|
|
2
|
+
|
|
3
|
+
Generalises a pattern proven in production (CaringCent BackOffice): a
|
|
4
|
+
``sys_feature_flags`` table that holds **one row per system**, keyed by an
|
|
5
|
+
``applied_system`` column, where every copy of the database carries every
|
|
6
|
+
system's row. Two behaviours follow:
|
|
7
|
+
|
|
8
|
+
* **Reads are scoped to the current system.** A bare ``find({})`` is wrong the
|
|
9
|
+
moment a second system's row exists — it returns an arbitrary one. Callers go
|
|
10
|
+
through :func:`get`, which selects the row for :func:`current_system`.
|
|
11
|
+
* **Writes mirror to the alternate database.** demo and production are separate
|
|
12
|
+
databases on the same instance; a flag saved in either is copied to the other
|
|
13
|
+
(matched by ``applied_system``, never ``sys_id`` — row ids differ between
|
|
14
|
+
databases) so the two converge no matter where the edit happened, and a
|
|
15
|
+
demo refresh preserves the demo row. :func:`mirror_row` does one row (the
|
|
16
|
+
real-time hook); :func:`reconcile` does all rows (the catch-up after the
|
|
17
|
+
alternate was unreachable, or a fresh environment).
|
|
18
|
+
|
|
19
|
+
velocity owns only the **mechanism**. The flag columns and what they mean stay
|
|
20
|
+
in each app's schema and code — this module never names a business flag. The
|
|
21
|
+
row key is the runtime **stage** (``demo`` / ``production``), so every database
|
|
22
|
+
carries one row per stage and each environment reads its own; the alternate
|
|
23
|
+
database name comes from config (:data:`ALTERNATE_DB_CONFIG_KEY`). (An app that
|
|
24
|
+
one day consolidates several systems into one database can widen the key then;
|
|
25
|
+
today the stage is the whole identity.)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
|
|
32
|
+
log = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
SYSTEM_COLUMN = "applied_system"
|
|
35
|
+
DEFAULT_TABLE = "sys_feature_flags"
|
|
36
|
+
ALTERNATE_DB_CONFIG_KEY = "DBAlternate"
|
|
37
|
+
|
|
38
|
+
_alternate_engines: dict = {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def current_system(default: str | None = None) -> str | None:
|
|
42
|
+
"""The ``applied_system`` key for the running environment — the runtime
|
|
43
|
+
stage (``demo`` / ``production``). Every database carries one row per stage;
|
|
44
|
+
this selects the running one.
|
|
45
|
+
"""
|
|
46
|
+
from velocity.aws import ssm_config
|
|
47
|
+
|
|
48
|
+
return (ssm_config.get_stage() or "").strip() or default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get(tx, system: str | None = None, *, table: str = DEFAULT_TABLE,
|
|
52
|
+
raise_on_missing: bool = False) -> dict:
|
|
53
|
+
"""The feature-flag row for ``system`` (defaults to :func:`current_system`).
|
|
54
|
+
|
|
55
|
+
Returns a plain dict; ``{}`` when no row matches unless ``raise_on_missing``.
|
|
56
|
+
"""
|
|
57
|
+
system = system if system is not None else current_system()
|
|
58
|
+
row = tx.table(table).find({SYSTEM_COLUMN: system})
|
|
59
|
+
if not row:
|
|
60
|
+
if raise_on_missing:
|
|
61
|
+
raise KeyError(f"no {table} row for {SYSTEM_COLUMN}={system!r}")
|
|
62
|
+
return {}
|
|
63
|
+
return dict(row)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _without_sys(row) -> dict:
|
|
67
|
+
"""A row's business columns — the ``sys_*`` bookkeeping never crosses over."""
|
|
68
|
+
return {k: v for k, v in dict(row).items() if not str(k).startswith("sys_")}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def alternate_engine(alternate_db: str | None = None):
|
|
72
|
+
"""Engine for the alternate database — same instance, other ``database``.
|
|
73
|
+
|
|
74
|
+
Host/port/user/password are inherited from the running config, so no
|
|
75
|
+
separate credentials are needed; only the database name changes. Cached per
|
|
76
|
+
database name so repeated mirrors in one process reuse the connection.
|
|
77
|
+
"""
|
|
78
|
+
from velocity.aws import ssm_config
|
|
79
|
+
from velocity.db.servers import postgres
|
|
80
|
+
|
|
81
|
+
if alternate_db is None:
|
|
82
|
+
alternate_db = ssm_config.getenv(ALTERNATE_DB_CONFIG_KEY, raise_on_missing=True)
|
|
83
|
+
if alternate_db not in _alternate_engines:
|
|
84
|
+
_alternate_engines[alternate_db] = postgres.initialize({"database": alternate_db})
|
|
85
|
+
return _alternate_engines[alternate_db]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mirror_row(row, *, alternate_db: str | None = None, engine=None,
|
|
89
|
+
table: str = DEFAULT_TABLE) -> str | None:
|
|
90
|
+
"""Upsert one flags row into the alternate database by ``applied_system``.
|
|
91
|
+
|
|
92
|
+
Returns the mirrored key, or ``None`` when the row has no system key (a
|
|
93
|
+
keyless row is skipped, never mirrored to an arbitrary target).
|
|
94
|
+
"""
|
|
95
|
+
data = _without_sys(row)
|
|
96
|
+
key = data.get(SYSTEM_COLUMN)
|
|
97
|
+
if not key:
|
|
98
|
+
return None
|
|
99
|
+
engine = engine or alternate_engine(alternate_db)
|
|
100
|
+
with engine.transaction() as alt:
|
|
101
|
+
alt.table(table).merge(data, pk={SYSTEM_COLUMN: key})
|
|
102
|
+
return key
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def reconcile(tx, *, alternate_db: str | None = None, engine=None,
|
|
106
|
+
table: str = DEFAULT_TABLE) -> dict:
|
|
107
|
+
"""Mirror every flags row into the alternate database. Catch-up companion of
|
|
108
|
+
:func:`mirror_row`. Returns ``{"synced": [...], "skipped": [...]}``."""
|
|
109
|
+
engine = engine or alternate_engine(alternate_db)
|
|
110
|
+
synced: list = []
|
|
111
|
+
skipped: list = []
|
|
112
|
+
rows = list(tx.table(table).select())
|
|
113
|
+
with engine.transaction() as alt:
|
|
114
|
+
for row in rows:
|
|
115
|
+
data = _without_sys(row)
|
|
116
|
+
key = data.get(SYSTEM_COLUMN)
|
|
117
|
+
if not key:
|
|
118
|
+
skipped.append(dict(row).get("sys_id"))
|
|
119
|
+
continue
|
|
120
|
+
alt.table(table).merge(data, pk={SYSTEM_COLUMN: key})
|
|
121
|
+
synced.append(key)
|
|
122
|
+
return {"synced": synced, "skipped": skipped}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _toast(context, message: str, level: str) -> None:
|
|
126
|
+
"""Best-effort operator toast; a context without one is fine (a queue run)."""
|
|
127
|
+
response = getattr(context, "response", None)
|
|
128
|
+
toast = getattr(response() if callable(response) else None, "toast", None)
|
|
129
|
+
if callable(toast):
|
|
130
|
+
toast(message, level)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def make_mirror_hook(*, alternate_db: str | None = None,
|
|
134
|
+
table: str = DEFAULT_TABLE, notify: bool = True):
|
|
135
|
+
"""An RWX ``after_new`` / ``after_write`` callable that mirrors the saved
|
|
136
|
+
row to the alternate database.
|
|
137
|
+
|
|
138
|
+
This is how the sync stays in ONE place: the app's ``rwx/<table>.py`` is a
|
|
139
|
+
two-line re-export (``after_new = after_write = make_mirror_hook()``) — the
|
|
140
|
+
logic lives here in velocity, not copied per repo. Warn-and-continue: a flag
|
|
141
|
+
save is never blocked by the alternate database being unreachable; the
|
|
142
|
+
:func:`reconcile` action (also here) is the catch-up.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def hook(tx, table_name, sys_id, row, context):
|
|
146
|
+
del tx, table_name, sys_id
|
|
147
|
+
try:
|
|
148
|
+
key = mirror_row(row, alternate_db=alternate_db, table=table)
|
|
149
|
+
if notify and key:
|
|
150
|
+
_toast(context, f"Feature flags for '{key}' mirrored to the "
|
|
151
|
+
"alternate database.", "success")
|
|
152
|
+
except Exception as exc: # warn-and-continue — never block the save
|
|
153
|
+
log.warning("feature-flag mirror to alternate DB failed", exc_info=True)
|
|
154
|
+
if notify:
|
|
155
|
+
_toast(context, "Flag change saved HERE but NOT mirrored to the "
|
|
156
|
+
f"alternate database ({exc}). Run the reconcile once it "
|
|
157
|
+
"is reachable.", "warning")
|
|
158
|
+
|
|
159
|
+
return hook
|
{velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity/aws/handlers/mixins/__init__.py
RENAMED
|
@@ -6,15 +6,19 @@ This package provides mixins for common Lambda handler patterns:
|
|
|
6
6
|
- ButtonHandler: Dynamic button action handler loading
|
|
7
7
|
- DataServiceMixin: Generic CRUD operations for database-backed APIs
|
|
8
8
|
- JobHandler: get-job-status / download-job-result / cancel-job for async jobs
|
|
9
|
+
- SessionControlMixin: opt-in single-active-login enforcement (register /
|
|
10
|
+
check / end session actions; pairs with velocity-react's SessionGate)
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from .web_handler import WebHandler, ButtonHandler
|
|
12
14
|
from .data_service import DataServiceMixin
|
|
13
15
|
from .job_handler import JobHandler
|
|
16
|
+
from .session_control import SessionControlMixin
|
|
14
17
|
|
|
15
18
|
__all__ = [
|
|
16
19
|
"WebHandler",
|
|
17
20
|
"ButtonHandler",
|
|
18
21
|
"DataServiceMixin",
|
|
19
22
|
"JobHandler",
|
|
23
|
+
"SessionControlMixin",
|
|
20
24
|
]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Single-active-session enforcement for handler-backed apps.
|
|
2
|
+
|
|
3
|
+
One live login per user: at sign-in the client registers a per-browser
|
|
4
|
+
session key; if another live session exists (and the user row's
|
|
5
|
+
``allow_multiple_sessions`` switch is off) the register answers
|
|
6
|
+
``conflict`` with the other session's ip / browser / times so the UI can
|
|
7
|
+
offer the choice — take over (ends the other session; its next heartbeat
|
|
8
|
+
signs it out) or back out of this sign-in. A periodic client heartbeat
|
|
9
|
+
keeps ``last_seen_at`` fresh and learns when it has been superseded;
|
|
10
|
+
sessions quiet past ``session_stale_minutes`` are expired server-side so a
|
|
11
|
+
crashed browser can't hold the account hostage.
|
|
12
|
+
|
|
13
|
+
Feature switch — OFF by default (opt-in per app):
|
|
14
|
+
- class attribute ``single_session_enforcement = True`` turns it on;
|
|
15
|
+
- the ``SingleSessionEnforcement`` config key overrides per environment
|
|
16
|
+
(same precedence pattern as ``QueryMaxRows``).
|
|
17
|
+
When off, every action answers as if the session were fine, so a deployed
|
|
18
|
+
frontend gate (velocity-react ``SessionGate``) becomes a no-op without a
|
|
19
|
+
frontend change.
|
|
20
|
+
|
|
21
|
+
Everything is scoped by the AUTHENTICATED session's email — the payload
|
|
22
|
+
only supplies the caller's own session key, so no action can read or end
|
|
23
|
+
another user's sessions. Sessions live in ``session_table`` (default
|
|
24
|
+
``app_session``; auto-created on first write under unlocked auto-schema —
|
|
25
|
+
locked-schema apps declare it in their ensure_schema). The per-user
|
|
26
|
+
switch column is read from the handler's ``user_table``.
|
|
27
|
+
|
|
28
|
+
Usage::
|
|
29
|
+
|
|
30
|
+
class HttpEventHandler(SessionControlMixin, WebHandler, LambdaHandler):
|
|
31
|
+
user_table = "app_user"
|
|
32
|
+
single_session_enforcement = True
|
|
33
|
+
|
|
34
|
+
Promoted 2026-09-07 from dance.10sp.us (same lifecycle as tenspace's
|
|
35
|
+
app_auth: proven in the app, then centralized).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import datetime
|
|
39
|
+
|
|
40
|
+
from velocity.aws import ssm_config
|
|
41
|
+
|
|
42
|
+
_TRUE = {"1", "true", "yes", "on"}
|
|
43
|
+
_FALSE = {"0", "false", "no", "off"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _now():
|
|
47
|
+
return datetime.datetime.now(datetime.timezone.utc)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _iso(value):
|
|
51
|
+
return value.isoformat() if hasattr(value, "isoformat") else (value or None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def describe_browser(user_agent):
|
|
55
|
+
"""Friendly 'Chrome on Windows' from a raw UA (best-effort, no deps)."""
|
|
56
|
+
ua = user_agent or ""
|
|
57
|
+
if "Edg/" in ua:
|
|
58
|
+
browser = "Edge"
|
|
59
|
+
elif "OPR/" in ua or "Opera" in ua:
|
|
60
|
+
browser = "Opera"
|
|
61
|
+
elif "Chrome/" in ua:
|
|
62
|
+
browser = "Chrome"
|
|
63
|
+
elif "Firefox/" in ua:
|
|
64
|
+
browser = "Firefox"
|
|
65
|
+
elif "Safari/" in ua:
|
|
66
|
+
browser = "Safari"
|
|
67
|
+
else:
|
|
68
|
+
browser = "Unknown browser"
|
|
69
|
+
# order matters: Android UAs contain "Linux", iPads report "Mac OS X"
|
|
70
|
+
if "Windows" in ua:
|
|
71
|
+
os_name = "Windows"
|
|
72
|
+
elif "Android" in ua:
|
|
73
|
+
os_name = "Android"
|
|
74
|
+
elif "iPhone" in ua or "iPad" in ua:
|
|
75
|
+
os_name = "iPhone/iPad"
|
|
76
|
+
elif "Mac OS X" in ua:
|
|
77
|
+
os_name = "Mac"
|
|
78
|
+
elif "Linux" in ua:
|
|
79
|
+
os_name = "Linux"
|
|
80
|
+
else:
|
|
81
|
+
os_name = ""
|
|
82
|
+
return f"{browser} on {os_name}" if os_name else browser
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def client_info(aws_event):
|
|
86
|
+
"""(ip, user_agent) from the API Gateway event — server-derived, never
|
|
87
|
+
trusted from the payload."""
|
|
88
|
+
identity = (aws_event or {}).get("requestContext", {}).get("identity", {})
|
|
89
|
+
headers = (aws_event or {}).get("headers") or {}
|
|
90
|
+
ip = identity.get("sourceIp") or ""
|
|
91
|
+
user_agent = headers.get("User-Agent") or headers.get("user-agent") or ""
|
|
92
|
+
return ip, user_agent
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _describe(row):
|
|
96
|
+
return {
|
|
97
|
+
"ip": row.get("ip") or "",
|
|
98
|
+
"browser": row.get("browser") or row.get("user_agent") or "",
|
|
99
|
+
"started_at": _iso(row.get("started_at")),
|
|
100
|
+
"last_seen_at": _iso(row.get("last_seen_at")),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SessionControlMixin:
|
|
105
|
+
"""register-session / check-session / end-session for the handler.
|
|
106
|
+
|
|
107
|
+
Add the three action names to any non-staff/authenticated-only allowlist
|
|
108
|
+
the handler enforces — every signed-in user needs them.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
# The app-level switch (see module docstring). The
|
|
112
|
+
# ``SingleSessionEnforcement`` config key overrides per environment.
|
|
113
|
+
single_session_enforcement = False
|
|
114
|
+
|
|
115
|
+
session_table = "app_session"
|
|
116
|
+
session_stale_minutes = 10
|
|
117
|
+
# user-table column holding the per-user opt-out of the single limit
|
|
118
|
+
session_multi_column = "allow_multiple_sessions"
|
|
119
|
+
|
|
120
|
+
# ── switch resolution ────────────────────────────────────────────────
|
|
121
|
+
def _session_enforcement_enabled(self):
|
|
122
|
+
raw = None
|
|
123
|
+
try:
|
|
124
|
+
raw = ssm_config.getenv("SingleSessionEnforcement", None)
|
|
125
|
+
except Exception:
|
|
126
|
+
raw = None
|
|
127
|
+
if raw is not None:
|
|
128
|
+
lowered = str(raw).strip().lower()
|
|
129
|
+
if lowered in _TRUE:
|
|
130
|
+
return True
|
|
131
|
+
if lowered in _FALSE:
|
|
132
|
+
return False
|
|
133
|
+
return bool(self.single_session_enforcement)
|
|
134
|
+
|
|
135
|
+
def _session_user_table(self):
|
|
136
|
+
return getattr(self, "user_table", None) or "app_user"
|
|
137
|
+
|
|
138
|
+
# ── shared internals ─────────────────────────────────────────────────
|
|
139
|
+
def _session_email(self, context):
|
|
140
|
+
return ((context.session() or {}).get("email_address") or "").strip().lower()
|
|
141
|
+
|
|
142
|
+
def _live_sessions(self, tx, email, now):
|
|
143
|
+
"""Live sessions for the user, expiring quiet ones on the way."""
|
|
144
|
+
sessions = tx.table(self.session_table)
|
|
145
|
+
stale_after = datetime.timedelta(minutes=self.session_stale_minutes)
|
|
146
|
+
live = []
|
|
147
|
+
for row in sessions.select(where={"email_address": email, "ended_at": None}):
|
|
148
|
+
seen = row.get("last_seen_at") or row.get("started_at")
|
|
149
|
+
if seen and (now - seen) > stale_after:
|
|
150
|
+
sessions.update(
|
|
151
|
+
{"ended_at": now, "ended_by": "expired (no heartbeat)"},
|
|
152
|
+
where={"sys_id": row["sys_id"]},
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
live.append(row)
|
|
156
|
+
return live
|
|
157
|
+
|
|
158
|
+
def _allow_multiple(self, tx, email):
|
|
159
|
+
row = next(
|
|
160
|
+
iter(
|
|
161
|
+
tx.table(self._session_user_table()).select(
|
|
162
|
+
where={"email_address": email}, qty=1
|
|
163
|
+
)
|
|
164
|
+
),
|
|
165
|
+
None,
|
|
166
|
+
)
|
|
167
|
+
return bool(row and row.get(self.session_multi_column))
|
|
168
|
+
|
|
169
|
+
# ── actions ──────────────────────────────────────────────────────────
|
|
170
|
+
def OnActionRegisterSession(self, tx, context):
|
|
171
|
+
"""Claim (or refresh) the caller's session; 'conflict' when someone
|
|
172
|
+
else is signed in, the user's multi-session switch is off, and this
|
|
173
|
+
isn't a takeover."""
|
|
174
|
+
payload = context.payload() or {}
|
|
175
|
+
email = self._session_email(context)
|
|
176
|
+
session_key = payload.get("session_key")
|
|
177
|
+
if not (email and session_key) or not self._session_enforcement_enabled():
|
|
178
|
+
context.response().set_body({"status": "active"})
|
|
179
|
+
return
|
|
180
|
+
ip, user_agent = client_info(self.aws_event)
|
|
181
|
+
now = _now()
|
|
182
|
+
sessions = tx.table(self.session_table)
|
|
183
|
+
|
|
184
|
+
mine = next(
|
|
185
|
+
iter(sessions.select(where={"session_key": session_key}, qty=1)), None
|
|
186
|
+
)
|
|
187
|
+
others = [
|
|
188
|
+
r
|
|
189
|
+
for r in self._live_sessions(tx, email, now)
|
|
190
|
+
if r.get("session_key") != session_key
|
|
191
|
+
]
|
|
192
|
+
takeover = bool(payload.get("takeover"))
|
|
193
|
+
|
|
194
|
+
if others and not takeover and not self._allow_multiple(tx, email):
|
|
195
|
+
# my previously-live row (a page refresh) doesn't excuse the
|
|
196
|
+
# conflict: only an ACTIVE claim survives, and mine may have
|
|
197
|
+
# been superseded
|
|
198
|
+
if not (mine and mine.get("ended_at") is None):
|
|
199
|
+
context.response().set_body(
|
|
200
|
+
{"status": "conflict", "sessions": [_describe(r) for r in others]}
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
if takeover:
|
|
205
|
+
for row in others:
|
|
206
|
+
sessions.update(
|
|
207
|
+
{
|
|
208
|
+
"ended_at": now,
|
|
209
|
+
"ended_by": (
|
|
210
|
+
f"signed out by a new sign-in from {ip or 'unknown ip'}"
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
where={"sys_id": row["sys_id"]},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
fields = {
|
|
217
|
+
"email_address": email,
|
|
218
|
+
"ip": ip,
|
|
219
|
+
"user_agent": user_agent,
|
|
220
|
+
"browser": describe_browser(user_agent),
|
|
221
|
+
"last_seen_at": now,
|
|
222
|
+
}
|
|
223
|
+
if mine and mine.get("ended_at") is None:
|
|
224
|
+
# same login continuing (page refresh) — keep started_at
|
|
225
|
+
sessions.update(fields, where={"sys_id": mine["sys_id"]})
|
|
226
|
+
else:
|
|
227
|
+
# brand-new sign-in, or reclaiming a key that was previously ended
|
|
228
|
+
fields.update({"started_at": now, "ended_at": None, "ended_by": None})
|
|
229
|
+
sessions.upsert(fields, {"session_key": session_key})
|
|
230
|
+
context.response().set_body({"status": "active"})
|
|
231
|
+
|
|
232
|
+
def OnActionCheckSession(self, tx, context):
|
|
233
|
+
"""Heartbeat: refresh last_seen_at, or report who superseded us."""
|
|
234
|
+
payload = context.payload() or {}
|
|
235
|
+
email = self._session_email(context)
|
|
236
|
+
session_key = payload.get("session_key")
|
|
237
|
+
if not (email and session_key) or not self._session_enforcement_enabled():
|
|
238
|
+
context.response().set_body({"status": "active"})
|
|
239
|
+
return
|
|
240
|
+
now = _now()
|
|
241
|
+
sessions = tx.table(self.session_table)
|
|
242
|
+
mine = next(
|
|
243
|
+
iter(
|
|
244
|
+
sessions.select(
|
|
245
|
+
where={"session_key": session_key, "email_address": email}, qty=1
|
|
246
|
+
)
|
|
247
|
+
),
|
|
248
|
+
None,
|
|
249
|
+
)
|
|
250
|
+
if mine and mine.get("ended_at") is None:
|
|
251
|
+
sessions.update({"last_seen_at": now}, where={"sys_id": mine["sys_id"]})
|
|
252
|
+
context.response().set_body({"status": "active"})
|
|
253
|
+
return
|
|
254
|
+
live = self._live_sessions(tx, email, now)
|
|
255
|
+
context.response().set_body(
|
|
256
|
+
{
|
|
257
|
+
"status": "superseded",
|
|
258
|
+
"reason": (mine or {}).get("ended_by") or "session ended",
|
|
259
|
+
"by": _describe(live[0]) if live else None,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def OnActionEndSession(self, tx, context):
|
|
264
|
+
"""Clean sign-out: release the caller's own session claim."""
|
|
265
|
+
payload = context.payload() or {}
|
|
266
|
+
email = self._session_email(context)
|
|
267
|
+
session_key = payload.get("session_key")
|
|
268
|
+
if email and session_key and self._session_enforcement_enabled():
|
|
269
|
+
tx.table(self.session_table).update(
|
|
270
|
+
{"ended_at": _now(), "ended_by": "signed out"},
|
|
271
|
+
where={
|
|
272
|
+
"session_key": session_key,
|
|
273
|
+
"email_address": email,
|
|
274
|
+
"ended_at": None,
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
context.response().set_body({"status": "ended"})
|
|
@@ -161,12 +161,23 @@ def _prepend_env_path(name: str, *paths: str) -> None:
|
|
|
161
161
|
os.environ[name] = os.pathsep.join(merged)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def _configure_layer_fonts() -> None:
|
|
165
|
+
"""Point fontconfig at a Lambda layer's /opt/fonts when nothing else has.
|
|
166
|
+
|
|
167
|
+
Must run BEFORE the first render in the process: with no usable
|
|
168
|
+
fontconfig config Pango gets a NULL font and the whole runtime dies with
|
|
169
|
+
a segmentation fault, not a Python error (found live 2026-09-07 on an
|
|
170
|
+
AL2023/python3.12 stack whose functions did not set FONTCONFIG_PATH).
|
|
171
|
+
Harmless off-Lambda — the directory simply doesn't exist."""
|
|
172
|
+
if os.path.isdir("/opt/fonts") and not os.environ.get("FONTCONFIG_PATH"):
|
|
173
|
+
os.environ["FONTCONFIG_PATH"] = "/opt/fonts"
|
|
174
|
+
|
|
175
|
+
|
|
164
176
|
def _configure_wkhtml_runtime() -> None:
|
|
165
177
|
# Lambda layers put the binary in /opt/bin and its shared libs in /opt/lib.
|
|
166
178
|
_prepend_env_path("PATH", "/opt/bin")
|
|
167
179
|
_prepend_env_path("LD_LIBRARY_PATH", "/opt/lib")
|
|
168
|
-
|
|
169
|
-
os.environ["FONTCONFIG_PATH"] = "/opt/fonts"
|
|
180
|
+
_configure_layer_fonts()
|
|
170
181
|
|
|
171
182
|
|
|
172
183
|
def _find_wkhtmltopdf() -> str | None:
|
|
@@ -351,6 +362,7 @@ class WeasyPrintRenderer(BasePdfRenderer):
|
|
|
351
362
|
"that carries its Pango/Cairo libraries and set LD_LIBRARY_PATH=/opt/lib, "
|
|
352
363
|
f"FONTCONFIG_PATH=/opt/fonts). Root cause: {_weasyprint_import_error!r}"
|
|
353
364
|
)
|
|
365
|
+
_configure_layer_fonts()
|
|
354
366
|
css_text = page_css(page)
|
|
355
367
|
if extra_css:
|
|
356
368
|
css_text += "\n" + extra_css
|
{velocity_python-0.1.103 → velocity_python-0.1.105}/src/velocity_python.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,7 @@ src/velocity/aws/amplify_build.py
|
|
|
9
9
|
src/velocity/aws/blobs.py
|
|
10
10
|
src/velocity/aws/dirty_pipeline.py
|
|
11
11
|
src/velocity/aws/email.py
|
|
12
|
+
src/velocity/aws/feature_flags.py
|
|
12
13
|
src/velocity/aws/job_results.py
|
|
13
14
|
src/velocity/aws/s3.py
|
|
14
15
|
src/velocity/aws/sms.py
|
|
@@ -38,6 +39,7 @@ src/velocity/aws/handlers/sqs_handler.py
|
|
|
38
39
|
src/velocity/aws/handlers/mixins/__init__.py
|
|
39
40
|
src/velocity/aws/handlers/mixins/data_service.py
|
|
40
41
|
src/velocity/aws/handlers/mixins/job_handler.py
|
|
42
|
+
src/velocity/aws/handlers/mixins/session_control.py
|
|
41
43
|
src/velocity/aws/handlers/mixins/web_handler.py
|
|
42
44
|
src/velocity/aws/tests/__init__.py
|
|
43
45
|
src/velocity/aws/tests/test_amplify_build_policy_size.py
|
|
@@ -212,6 +214,7 @@ tests/test_delete_hook_fk_blocked.py
|
|
|
212
214
|
tests/test_dirty_pipeline_fast_path.py
|
|
213
215
|
tests/test_email_processing.py
|
|
214
216
|
tests/test_enqueue_send_failures.py
|
|
217
|
+
tests/test_feature_flags.py
|
|
215
218
|
tests/test_get_cognito_user_provider.py
|
|
216
219
|
tests/test_http_handler_rollback.py
|
|
217
220
|
tests/test_iconv_money_to_cents.py
|
|
@@ -254,6 +257,7 @@ tests/test_row_cache_staleness.py
|
|
|
254
257
|
tests/test_schema_migrations.py
|
|
255
258
|
tests/test_security_hardening.py
|
|
256
259
|
tests/test_server_cursor.py
|
|
260
|
+
tests/test_session_control.py
|
|
257
261
|
tests/test_single_autocommit_safety.py
|
|
258
262
|
tests/test_spreadsheet_functions.py
|
|
259
263
|
tests/test_sqlite_backend.py
|