velocity-python 0.1.21__tar.gz → 0.1.23__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.21/src/velocity_python.egg-info → velocity_python-0.1.23}/PKG-INFO +1 -1
- {velocity_python-0.1.21 → velocity_python-0.1.23}/pyproject.toml +1 -1
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/__init__.py +20 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/amplify_build.py +62 -31
- velocity_python-0.1.23/src/velocity/aws/s3.py +175 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/async_support.py +10 -2
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/transaction.py +16 -2
- {velocity_python-0.1.21 → velocity_python-0.1.23/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- velocity_python-0.1.23/tests/test_amplify_build.py +150 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_async_support.py +5 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_observability.py +3 -0
- velocity_python-0.1.21/tests/test_amplify_build.py +0 -70
- {velocity_python-0.1.21 → velocity_python-0.1.23}/LICENSE +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/README.md +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/setup.cfg +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_row_dirty_tracking.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.21 → velocity_python-0.1.23}/tests/test_where_clause_validation.py +0 -0
|
@@ -28,6 +28,16 @@ _LAZY_ATTRS = {
|
|
|
28
28
|
"initialize_build_environment": "velocity.aws.amplify_build",
|
|
29
29
|
"run_backend_deployment": "velocity.aws.amplify_build",
|
|
30
30
|
"update_lambda_layers_to_latest": "velocity.aws.amplify_build",
|
|
31
|
+
# S3 utilities
|
|
32
|
+
"generate_presigned_get": "velocity.aws.s3",
|
|
33
|
+
"generate_presigned_put": "velocity.aws.s3",
|
|
34
|
+
"list_client_files": "velocity.aws.s3",
|
|
35
|
+
"normalize_client_prefix": "velocity.aws.s3",
|
|
36
|
+
"CLIENT_FILES_BUCKET": "velocity.aws.s3",
|
|
37
|
+
"PUBLIC_ASSETS_BUCKET": "velocity.aws.s3",
|
|
38
|
+
"BACKOFFICE_BUCKET": "velocity.aws.s3",
|
|
39
|
+
"PRIVATE_BUCKETS": "velocity.aws.s3",
|
|
40
|
+
"ALLOWED_UPLOAD_BUCKETS": "velocity.aws.s3",
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
|
|
@@ -47,4 +57,14 @@ __all__ = [
|
|
|
47
57
|
"initialize_build_environment",
|
|
48
58
|
"run_backend_deployment",
|
|
49
59
|
"update_lambda_layers_to_latest",
|
|
60
|
+
# S3 utilities
|
|
61
|
+
"generate_presigned_get",
|
|
62
|
+
"generate_presigned_put",
|
|
63
|
+
"list_client_files",
|
|
64
|
+
"normalize_client_prefix",
|
|
65
|
+
"CLIENT_FILES_BUCKET",
|
|
66
|
+
"PUBLIC_ASSETS_BUCKET",
|
|
67
|
+
"BACKOFFICE_BUCKET",
|
|
68
|
+
"PRIVATE_BUCKETS",
|
|
69
|
+
"ALLOWED_UPLOAD_BUCKETS",
|
|
50
70
|
]
|
|
@@ -235,6 +235,36 @@ def retryable_call(
|
|
|
235
235
|
raise
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
def resolve_latest_layer_arns(
|
|
239
|
+
lambda_client,
|
|
240
|
+
current_layers: Sequence[Mapping[str, Any]],
|
|
241
|
+
) -> Sequence[str]:
|
|
242
|
+
updated_layer_arns = []
|
|
243
|
+
|
|
244
|
+
for layer in current_layers or ():
|
|
245
|
+
layer_arn = layer["Arn"]
|
|
246
|
+
layer_name = layer_arn.split(":")[-2]
|
|
247
|
+
current_version = layer_arn.split(":")[-1]
|
|
248
|
+
|
|
249
|
+
latest_response = retryable_call(
|
|
250
|
+
lambda_client.list_layer_versions,
|
|
251
|
+
LayerName=layer_name,
|
|
252
|
+
MaxItems=1,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if latest_response and latest_response.get("LayerVersions"):
|
|
256
|
+
latest_version_info = latest_response["LayerVersions"][0]
|
|
257
|
+
latest_version = latest_version_info["Version"]
|
|
258
|
+
latest_arn = latest_version_info["LayerVersionArn"]
|
|
259
|
+
updated_layer_arns.append(
|
|
260
|
+
latest_arn if str(latest_version) != str(current_version) else layer_arn
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
updated_layer_arns.append(layer_arn)
|
|
264
|
+
|
|
265
|
+
return updated_layer_arns
|
|
266
|
+
|
|
267
|
+
|
|
238
268
|
def _serialize_policy_document(policy_document: Any) -> str:
|
|
239
269
|
if isinstance(policy_document, str):
|
|
240
270
|
return policy_document
|
|
@@ -443,6 +473,16 @@ def run_backend_deployment(
|
|
|
443
473
|
|
|
444
474
|
timeout_value = config.resolve_timeout(function_name)
|
|
445
475
|
memory_value = config.resolve_memory_size(function_name)
|
|
476
|
+
layers_value = None
|
|
477
|
+
current_layers = function.get("Layers") or []
|
|
478
|
+
if current_layers:
|
|
479
|
+
try:
|
|
480
|
+
layers_value = list(resolve_latest_layer_arns(lambda_client, current_layers))
|
|
481
|
+
except Exception as exc:
|
|
482
|
+
print(
|
|
483
|
+
f"[WARN] Unable to resolve latest layers for {function_name}: {exc}. "
|
|
484
|
+
"Keeping current layer attachments."
|
|
485
|
+
)
|
|
446
486
|
print(
|
|
447
487
|
f"[INFO] Updating Lambda function {function_name} with timeout={timeout_value}"
|
|
448
488
|
+ (
|
|
@@ -456,6 +496,7 @@ def run_backend_deployment(
|
|
|
456
496
|
memory_size=memory_value,
|
|
457
497
|
subnet_ids=subnet_ids,
|
|
458
498
|
security_group_ids=security_group_ids,
|
|
499
|
+
layers=layers_value,
|
|
459
500
|
)
|
|
460
501
|
|
|
461
502
|
if not set_cloudwatch_logging(
|
|
@@ -563,44 +604,34 @@ def update_lambda_layers_to_latest(
|
|
|
563
604
|
layer_arn = layer["Arn"]
|
|
564
605
|
layer_name = layer_arn.split(":")[-2]
|
|
565
606
|
current_version = layer_arn.split(":")[-1]
|
|
566
|
-
|
|
567
607
|
print(
|
|
568
608
|
f"[INFO] Processing layer: {layer_name} (current version: {current_version})"
|
|
569
609
|
)
|
|
570
610
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
611
|
+
try:
|
|
612
|
+
updated_layer_arns = list(
|
|
613
|
+
resolve_latest_layer_arns(lambda_client, current_layers)
|
|
614
|
+
)
|
|
615
|
+
except Exception as exc:
|
|
616
|
+
print(f"[ERROR] Failed to resolve latest layers: {exc}")
|
|
617
|
+
print("[INFO] Keeping current layer versions")
|
|
618
|
+
updated_layer_arns = [layer["Arn"] for layer in current_layers]
|
|
619
|
+
|
|
620
|
+
for original_arn, updated_arn in zip(
|
|
621
|
+
[layer["Arn"] for layer in current_layers],
|
|
622
|
+
updated_layer_arns,
|
|
623
|
+
):
|
|
624
|
+
layer_name = original_arn.split(":")[-2]
|
|
625
|
+
current_version = original_arn.split(":")[-1]
|
|
626
|
+
latest_version = updated_arn.split(":")[-1]
|
|
627
|
+
if updated_arn != original_arn:
|
|
628
|
+
print(
|
|
629
|
+
f"[INFO] Updating {layer_name}: v{current_version} -> v{latest_version}"
|
|
576
630
|
)
|
|
577
|
-
|
|
578
|
-
if latest_response and latest_response.get("LayerVersions"):
|
|
579
|
-
latest_version_info = latest_response["LayerVersions"][0]
|
|
580
|
-
latest_version = latest_version_info["Version"]
|
|
581
|
-
latest_arn = latest_version_info["LayerVersionArn"]
|
|
582
|
-
|
|
583
|
-
if str(latest_version) != str(current_version):
|
|
584
|
-
print(
|
|
585
|
-
f"[INFO] Updating {layer_name}: v{current_version} -> v{latest_version}"
|
|
586
|
-
)
|
|
587
|
-
updated_layer_arns.append(latest_arn)
|
|
588
|
-
else:
|
|
589
|
-
print(
|
|
590
|
-
f"[INFO] {layer_name} is already at latest version (v{latest_version})"
|
|
591
|
-
)
|
|
592
|
-
updated_layer_arns.append(layer_arn)
|
|
593
|
-
else:
|
|
594
|
-
print(
|
|
595
|
-
f"[WARN] No versions found for layer {layer_name}, keeping current version"
|
|
596
|
-
)
|
|
597
|
-
updated_layer_arns.append(layer_arn)
|
|
598
|
-
except Exception as exc:
|
|
631
|
+
else:
|
|
599
632
|
print(
|
|
600
|
-
f"[
|
|
633
|
+
f"[INFO] {layer_name} is already at latest version (v{latest_version})"
|
|
601
634
|
)
|
|
602
|
-
print(f"[INFO] Keeping current version for {layer_name}")
|
|
603
|
-
updated_layer_arns.append(layer_arn)
|
|
604
635
|
|
|
605
636
|
current_layer_arns = [layer["Arn"] for layer in current_layers]
|
|
606
637
|
if updated_layer_arns != current_layer_arns:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Centralized S3 utilities: presigned URL generation and private-file listing.
|
|
2
|
+
|
|
3
|
+
Architectural principles enforced here
|
|
4
|
+
---------------------------------------
|
|
5
|
+
* All files in *private* buckets (e.g. ``caringcent.client.files``) MUST be
|
|
6
|
+
stored without a public ACL. Access is granted exclusively through
|
|
7
|
+
time-limited presigned GET URLs generated by this module.
|
|
8
|
+
* Direct browser-to-S3 uploads are supported via presigned PUT URLs so that
|
|
9
|
+
file bytes never pass through Lambda (avoids the 6 MB API Gateway payload
|
|
10
|
+
ceiling and reduces Lambda cost).
|
|
11
|
+
* Public-asset buckets (``donate.resources``, ``backoffice.resources``) remain
|
|
12
|
+
unaffected by this module; they continue to use ``ACL: public-read`` when
|
|
13
|
+
uploaded via ``helpers.upload(public=True)``.
|
|
14
|
+
|
|
15
|
+
Bucket constants
|
|
16
|
+
-----------------
|
|
17
|
+
Import and compare against these rather than hard-coding bucket names in
|
|
18
|
+
Lambda handlers so a rename only needs to happen in one place.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
import boto3
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Bucket name constants
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
CLIENT_FILES_BUCKET: str = "caringcent.client.files"
|
|
30
|
+
"""Private bucket that stores per-client files (billing reports, uploads)."""
|
|
31
|
+
|
|
32
|
+
PUBLIC_ASSETS_BUCKET: str = "donate.resources"
|
|
33
|
+
"""Public bucket for images and form-builder assets embedded in donation forms."""
|
|
34
|
+
|
|
35
|
+
BACKOFFICE_BUCKET: str = "backoffice.resources"
|
|
36
|
+
"""Internal bucket for BackOffice admin assets (help videos, data-load ZIPs)."""
|
|
37
|
+
|
|
38
|
+
# Buckets that must never be served with a public ACL.
|
|
39
|
+
PRIVATE_BUCKETS: frozenset[str] = frozenset({CLIENT_FILES_BUCKET})
|
|
40
|
+
|
|
41
|
+
# Buckets that are permitted as upload destinations from BackOffice.
|
|
42
|
+
ALLOWED_UPLOAD_BUCKETS: frozenset[str] = frozenset(
|
|
43
|
+
{CLIENT_FILES_BUCKET, PUBLIC_ASSETS_BUCKET, BACKOFFICE_BUCKET}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Presigned URL helpers
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def generate_presigned_get(
|
|
52
|
+
bucket: str,
|
|
53
|
+
key: str,
|
|
54
|
+
*,
|
|
55
|
+
expires_in: int = 3600,
|
|
56
|
+
filename: str | None = None,
|
|
57
|
+
content_type: str | None = None,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Return a presigned GET URL for a private S3 object.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
bucket: S3 bucket name.
|
|
63
|
+
key: S3 object key.
|
|
64
|
+
expires_in: URL lifetime in seconds (default 1 hour).
|
|
65
|
+
filename: When provided the presigned URL includes a
|
|
66
|
+
``Content-Disposition: attachment; filename="..."`` override so the
|
|
67
|
+
browser downloads the file under *filename* instead of navigating
|
|
68
|
+
to the raw key. This also prevents the browser from navigating
|
|
69
|
+
away from the current SPA page.
|
|
70
|
+
content_type: Optional ``Content-Type`` override embedded in the
|
|
71
|
+
presigned URL query-string.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A time-limited, signed HTTPS URL that grants GET access to the object.
|
|
75
|
+
"""
|
|
76
|
+
params: dict = {"Bucket": bucket, "Key": key}
|
|
77
|
+
if filename:
|
|
78
|
+
params["ResponseContentDisposition"] = f'attachment; filename="{filename}"'
|
|
79
|
+
if content_type:
|
|
80
|
+
params["ResponseContentType"] = content_type
|
|
81
|
+
s3 = boto3.client("s3")
|
|
82
|
+
return s3.generate_presigned_url("get_object", Params=params, ExpiresIn=expires_in)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_presigned_put(
|
|
86
|
+
bucket: str,
|
|
87
|
+
key: str,
|
|
88
|
+
*,
|
|
89
|
+
content_type: str | None = None,
|
|
90
|
+
expires_in: int = 900,
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Return a presigned PUT URL for a direct browser-to-S3 upload.
|
|
93
|
+
|
|
94
|
+
Using this URL the browser PUTs the file body directly to S3, bypassing
|
|
95
|
+
Lambda entirely. This removes the 6 MB API Gateway payload limit and
|
|
96
|
+
reduces Lambda invocation cost for large files.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
bucket: S3 bucket name.
|
|
100
|
+
key: S3 object key for the uploaded file.
|
|
101
|
+
content_type: Optional MIME type. When supplied the client **must**
|
|
102
|
+
send the same ``Content-Type`` header in the PUT request, otherwise
|
|
103
|
+
S3 rejects the upload with a 403.
|
|
104
|
+
expires_in: URL lifetime in seconds (default 15 minutes).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict with keys ``url`` (the presigned PUT URL), ``key``, and
|
|
108
|
+
``bucket``. Pass this dict back to the frontend; the browser calls
|
|
109
|
+
``fetch(url, { method: 'PUT', body: file })`` to upload.
|
|
110
|
+
"""
|
|
111
|
+
params: dict = {"Bucket": bucket, "Key": key}
|
|
112
|
+
if content_type:
|
|
113
|
+
params["ContentType"] = content_type
|
|
114
|
+
s3 = boto3.client("s3")
|
|
115
|
+
url = s3.generate_presigned_url("put_object", Params=params, ExpiresIn=expires_in)
|
|
116
|
+
return {"url": url, "key": key, "bucket": bucket}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Object listing helpers
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def normalize_client_prefix(client_id: str) -> str:
|
|
124
|
+
"""Normalise *client_id* to the S3 key prefix used when storing files.
|
|
125
|
+
|
|
126
|
+
The convention (established by ``OnActionAdminUploadFiles``) is
|
|
127
|
+
``lowercase + spaces → hyphens``, matching the existing
|
|
128
|
+
``client_id.lower().replace(" ", "-")`` pattern.
|
|
129
|
+
"""
|
|
130
|
+
return re.sub(r"\s+", "-", (client_id or "").strip().lower())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def list_client_files(
|
|
134
|
+
bucket: str,
|
|
135
|
+
client_prefix: str,
|
|
136
|
+
*,
|
|
137
|
+
expires_in: int = 3600,
|
|
138
|
+
) -> list[dict]:
|
|
139
|
+
"""List non-empty objects under *client_prefix* and attach presigned URLs.
|
|
140
|
+
|
|
141
|
+
Zero-byte objects (S3 directory markers created by the console) are
|
|
142
|
+
silently skipped.
|
|
143
|
+
|
|
144
|
+
Each returned dict is the original S3 ``Contents`` entry enriched with:
|
|
145
|
+
|
|
146
|
+
* ``SignedUrl`` – presigned GET URL valid for *expires_in* seconds.
|
|
147
|
+
* ``Header`` – humanized sub-folder name (second-to-last key component),
|
|
148
|
+
used as the category label in :class:`S3DownloadsList`.
|
|
149
|
+
* ``FileName`` – the bare filename (last key component).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
bucket: S3 bucket name.
|
|
153
|
+
client_prefix: Key prefix, typically the normalized client ID.
|
|
154
|
+
expires_in: Presigned URL lifetime in seconds (default 1 hour).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of enriched object dicts, empty if the prefix has no objects.
|
|
158
|
+
"""
|
|
159
|
+
s3 = boto3.client("s3")
|
|
160
|
+
result = s3.list_objects_v2(Bucket=bucket, Prefix=client_prefix, MaxKeys=100)
|
|
161
|
+
enriched: list[dict] = []
|
|
162
|
+
for obj in result.get("Contents", []):
|
|
163
|
+
if obj.get("Size", 0) == 0:
|
|
164
|
+
continue # skip directory-marker objects
|
|
165
|
+
key: str = obj["Key"]
|
|
166
|
+
parts = key.split("/")
|
|
167
|
+
obj["SignedUrl"] = generate_presigned_get(bucket, key, expires_in=expires_in)
|
|
168
|
+
obj["Header"] = (
|
|
169
|
+
parts[-2].replace("_", " ").replace("-", " ").title()
|
|
170
|
+
if len(parts) > 1
|
|
171
|
+
else ""
|
|
172
|
+
)
|
|
173
|
+
obj["FileName"] = parts[-1]
|
|
174
|
+
enriched.append(obj)
|
|
175
|
+
return enriched
|
|
@@ -493,10 +493,18 @@ class AsyncTransaction:
|
|
|
493
493
|
|
|
494
494
|
# R12 — Slow query logging
|
|
495
495
|
if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
|
|
496
|
+
from velocity.db.core.transaction import _summarize_sql
|
|
497
|
+
|
|
498
|
+
sql_preview = _summarize_sql(sql)
|
|
496
499
|
_logger.warning(
|
|
497
500
|
"Slow async query: %.1f ms sql=%s",
|
|
498
|
-
elapsed_ms,
|
|
499
|
-
|
|
501
|
+
elapsed_ms,
|
|
502
|
+
sql_preview,
|
|
503
|
+
extra={
|
|
504
|
+
"query_duration_ms": round(elapsed_ms, 1),
|
|
505
|
+
"sql_preview": sql_preview,
|
|
506
|
+
},
|
|
507
|
+
stack_info=True,
|
|
500
508
|
)
|
|
501
509
|
|
|
502
510
|
# R14 — N+1 detection
|
|
@@ -23,6 +23,7 @@ _DEFAULT_QUERY_CACHE_SIZE = int(os.environ.get("VELOCITY_QUERY_CACHE_SIZE", "100
|
|
|
23
23
|
|
|
24
24
|
# Slow-query threshold in milliseconds (0 = disabled).
|
|
25
25
|
_SLOW_QUERY_MS = int(os.environ.get("VELOCITY_SLOW_QUERY_MS", "500"))
|
|
26
|
+
_SLOW_QUERY_SQL_CHARS = int(os.environ.get("VELOCITY_SLOW_QUERY_SQL_CHARS", "4000"))
|
|
26
27
|
|
|
27
28
|
# N+1 detection: warn when the same table is SELECTed more than this many
|
|
28
29
|
# times within a single transaction. 0 = disabled. Only active when the
|
|
@@ -76,6 +77,16 @@ def _extract_table_name(sql):
|
|
|
76
77
|
return None
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def _summarize_sql(sql, limit=None):
|
|
81
|
+
if not sql:
|
|
82
|
+
return None
|
|
83
|
+
compact = " ".join(str(sql).split())
|
|
84
|
+
max_chars = _SLOW_QUERY_SQL_CHARS if limit is None else limit
|
|
85
|
+
if max_chars and len(compact) > max_chars:
|
|
86
|
+
return compact[:max_chars] + "... [truncated]"
|
|
87
|
+
return compact
|
|
88
|
+
|
|
89
|
+
|
|
79
90
|
class Transaction:
|
|
80
91
|
"""
|
|
81
92
|
Encapsulates a single transaction in the database (connection + commit/rollback).
|
|
@@ -210,14 +221,17 @@ class Transaction:
|
|
|
210
221
|
if _SLOW_QUERY_MS and elapsed_ms > _SLOW_QUERY_MS:
|
|
211
222
|
op = _classify_sql(sql)
|
|
212
223
|
tbl = _extract_table_name(sql)
|
|
224
|
+
sql_preview = _summarize_sql(sql)
|
|
213
225
|
_logger.warning(
|
|
214
|
-
"Slow query (%s): %.1f ms table=%s",
|
|
215
|
-
op, elapsed_ms, tbl,
|
|
226
|
+
"Slow query (%s): %.1f ms table=%s sql=%s",
|
|
227
|
+
op, elapsed_ms, tbl, sql_preview,
|
|
216
228
|
extra={
|
|
217
229
|
"query_duration_ms": round(elapsed_ms, 1),
|
|
218
230
|
"table_name": tbl,
|
|
219
231
|
"operation": op,
|
|
232
|
+
"sql_preview": sql_preview,
|
|
220
233
|
},
|
|
234
|
+
stack_info=True,
|
|
221
235
|
)
|
|
222
236
|
|
|
223
237
|
# R14 — N+1 detection (only when debug=True).
|
|
@@ -6,6 +6,7 @@ src/velocity/logging.py
|
|
|
6
6
|
src/velocity/aws/__init__.py
|
|
7
7
|
src/velocity/aws/amplify.py
|
|
8
8
|
src/velocity/aws/amplify_build.py
|
|
9
|
+
src/velocity/aws/s3.py
|
|
9
10
|
src/velocity/aws/handlers/__init__.py
|
|
10
11
|
src/velocity/aws/handlers/base_handler.py
|
|
11
12
|
src/velocity/aws/handlers/context.py
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from velocity.aws.amplify_build import (
|
|
2
|
+
BackendDeploymentConfig,
|
|
3
|
+
build_environment_variables,
|
|
4
|
+
get_amplify_app_id_and_branch,
|
|
5
|
+
run_backend_deployment,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeAmplifyProject:
|
|
10
|
+
def get_merged_env_vars(self, branch):
|
|
11
|
+
return {"EXISTING": branch}
|
|
12
|
+
|
|
13
|
+
def get_region(self):
|
|
14
|
+
return "us-east-1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeDeployProject(FakeAmplifyProject):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.updated_functions = []
|
|
20
|
+
|
|
21
|
+
def list_lambda_functions_filtered(self, branch):
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
"FunctionName": f"ClientEvents-{branch}",
|
|
25
|
+
"Layers": [
|
|
26
|
+
{
|
|
27
|
+
"Arn": "arn:aws:lambda:us-east-1:741671896925:layer:py-lib-support:225"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"Arn": "arn:aws:lambda:us-east-1:741671896925:layer:py-lib-accounting:34"
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def update_lambda_function(self, **kwargs):
|
|
37
|
+
self.updated_functions.append(kwargs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FakeLambdaClient:
|
|
41
|
+
def list_layer_versions(self, LayerName, MaxItems):
|
|
42
|
+
versions = {
|
|
43
|
+
"py-lib-support": [
|
|
44
|
+
{
|
|
45
|
+
"Version": 240,
|
|
46
|
+
"LayerVersionArn": "arn:aws:lambda:us-east-1:741671896925:layer:py-lib-support:240",
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"py-lib-accounting": [
|
|
50
|
+
{
|
|
51
|
+
"Version": 34,
|
|
52
|
+
"LayerVersionArn": "arn:aws:lambda:us-east-1:741671896925:layer:py-lib-accounting:34",
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
return {"LayerVersions": versions[LayerName][:MaxItems]}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_backend_deployment_config_resolves_queue_timeout_and_memory_size():
|
|
60
|
+
config = BackendDeploymentConfig(
|
|
61
|
+
queue_name_template="clients-queue-{branch}",
|
|
62
|
+
short_timeout_tokens=("Events", "Data"),
|
|
63
|
+
memory_size_by_token={"QueueHandler": 1024, "Events": 512},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert config.resolve_queue_name("Demo") == "clients-queue-demo"
|
|
67
|
+
assert config.resolve_timeout("ClientEvents-demo") == 60
|
|
68
|
+
assert config.resolve_timeout("ClientUtility-demo") == 900
|
|
69
|
+
assert config.resolve_memory_size("ClientQueueHandler-demo") == 1024
|
|
70
|
+
assert config.resolve_memory_size("ClientEvents-demo") == 512
|
|
71
|
+
assert config.resolve_memory_size("ClientUtility-demo") is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_build_environment_variables_reads_project_metadata(tmp_path, monkeypatch):
|
|
75
|
+
amplify_dir = tmp_path / "amplify" / "backend"
|
|
76
|
+
amplify_dir.mkdir(parents=True)
|
|
77
|
+
(amplify_dir / "amplify-meta.json").write_text('{"UserPoolId": "us-east-1_demoPool",}')
|
|
78
|
+
|
|
79
|
+
monkeypatch.setenv("AWS_JOB_ID", "job-123")
|
|
80
|
+
app = FakeAmplifyProject()
|
|
81
|
+
|
|
82
|
+
env_vars = build_environment_variables(
|
|
83
|
+
app,
|
|
84
|
+
"demo",
|
|
85
|
+
"clients-queue-demo",
|
|
86
|
+
project_root=tmp_path,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert env_vars["EXISTING"] == "demo"
|
|
90
|
+
assert env_vars["SqsWorkQueue"] == "clients-queue-demo"
|
|
91
|
+
assert env_vars["AWS_JOB_ID"] == "job-123"
|
|
92
|
+
assert env_vars["AWS_USER_POOL_ID"] == "us-east-1_demoPool"
|
|
93
|
+
assert env_vars["USER_BRANCH"] == "demo"
|
|
94
|
+
assert env_vars["REACT_APP_USER_BRANCH"] == "demo"
|
|
95
|
+
assert env_vars["USER_REGION"] == "us-east-1"
|
|
96
|
+
assert env_vars["LOGLEVEL"] == "INFO"
|
|
97
|
+
assert "BUILD_DATETIME" in env_vars
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_get_amplify_app_id_and_branch_uses_team_provider_info(tmp_path, monkeypatch):
|
|
101
|
+
team_provider_path = tmp_path / "amplify"
|
|
102
|
+
team_provider_path.mkdir(parents=True)
|
|
103
|
+
(team_provider_path / "team-provider-info.json").write_text(
|
|
104
|
+
'{"demo": {"awscloudformation": {"AmplifyAppId": "app-123"}}}'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
monkeypatch.setenv("AWS_BRANCH", "demo")
|
|
108
|
+
monkeypatch.delenv("AWS_APP_ID", raising=False)
|
|
109
|
+
|
|
110
|
+
app_id, branch = get_amplify_app_id_and_branch(tmp_path)
|
|
111
|
+
|
|
112
|
+
assert app_id == "app-123"
|
|
113
|
+
assert branch == "demo"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_run_backend_deployment_refreshes_lambda_layers(monkeypatch, tmp_path):
|
|
117
|
+
monkeypatch.setattr(
|
|
118
|
+
"velocity.aws.amplify_build.ensure_lambda_policies_and_attach",
|
|
119
|
+
lambda *args, **kwargs: None,
|
|
120
|
+
)
|
|
121
|
+
monkeypatch.setattr(
|
|
122
|
+
"velocity.aws.amplify_build.set_cloudwatch_logging",
|
|
123
|
+
lambda *args, **kwargs: True,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
app = FakeDeployProject()
|
|
127
|
+
config = BackendDeploymentConfig(
|
|
128
|
+
queue_name_template="clients-queue-{branch}",
|
|
129
|
+
short_timeout_tokens=("Events",),
|
|
130
|
+
memory_size_by_token={"Events": 512},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
run_backend_deployment(
|
|
134
|
+
"app-123",
|
|
135
|
+
"demo",
|
|
136
|
+
config,
|
|
137
|
+
project_root=tmp_path,
|
|
138
|
+
app=app,
|
|
139
|
+
lambda_client=FakeLambdaClient(),
|
|
140
|
+
logs_client=object(),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
assert len(app.updated_functions) == 1
|
|
144
|
+
assert app.updated_functions[0]["function_name"] == "ClientEvents-demo"
|
|
145
|
+
assert app.updated_functions[0]["timeout"] == 60
|
|
146
|
+
assert app.updated_functions[0]["memory_size"] == 512
|
|
147
|
+
assert app.updated_functions[0]["layers"] == [
|
|
148
|
+
"arn:aws:lambda:us-east-1:741671896925:layer:py-lib-support:240",
|
|
149
|
+
"arn:aws:lambda:us-east-1:741671896925:layer:py-lib-accounting:34",
|
|
150
|
+
]
|
|
@@ -687,3 +687,8 @@ class TestAsyncSlowQueryLogging:
|
|
|
687
687
|
if "Slow" in str(c)
|
|
688
688
|
]
|
|
689
689
|
assert len(slow_calls) >= 1
|
|
690
|
+
args = slow_calls[0].args
|
|
691
|
+
kwargs = slow_calls[0].kwargs
|
|
692
|
+
assert args[2] == "SELECT * FROM large_table"
|
|
693
|
+
assert kwargs["extra"]["sql_preview"] == "SELECT * FROM large_table"
|
|
694
|
+
assert kwargs["stack_info"] is True
|
|
@@ -177,6 +177,7 @@ class TestSlowQueryLogging:
|
|
|
177
177
|
|
|
178
178
|
assert any("Slow query" in r.message for r in caplog.records)
|
|
179
179
|
assert any("big_table" in r.message for r in caplog.records)
|
|
180
|
+
assert any("sql=SELECT * FROM big_table" in r.message for r in caplog.records)
|
|
180
181
|
|
|
181
182
|
def test_fast_query_no_warning(self, caplog):
|
|
182
183
|
tx = _make_tx()
|
|
@@ -230,6 +231,8 @@ class TestSlowQueryLogging:
|
|
|
230
231
|
assert rec.query_duration_ms > 0
|
|
231
232
|
assert rec.table_name == "orders"
|
|
232
233
|
assert rec.operation == "SELECT"
|
|
234
|
+
assert rec.sql_preview == "SELECT * FROM orders WHERE id = 1"
|
|
235
|
+
assert rec.stack_info
|
|
233
236
|
|
|
234
237
|
|
|
235
238
|
# ──────────────────────────────────────────────────────────────────────
|