stackport 0.4.8__tar.gz → 0.4.9__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.
Files changed (176) hide show
  1. {stackport-0.4.8/stackport.egg-info → stackport-0.4.9}/PKG-INFO +4 -4
  2. {stackport-0.4.8 → stackport-0.4.9}/README.md +1 -1
  3. {stackport-0.4.8 → stackport-0.4.9}/backend/aws_client.py +8 -0
  4. {stackport-0.4.8 → stackport-0.4.9}/backend/cli.py +14 -65
  5. stackport-0.4.9/backend/downloads.py +64 -0
  6. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/verify.py +2 -7
  7. {stackport-0.4.8 → stackport-0.4.9}/backend/main.py +16 -2
  8. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/dynamodb.py +184 -171
  9. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/iam.py +17 -15
  10. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/lambda_svc.py +8 -35
  11. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/logs.py +27 -30
  12. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/resources.py +123 -96
  13. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/s3.py +49 -14
  14. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/sqs.py +48 -31
  15. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/stats.py +51 -11
  16. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/tags.py +51 -20
  17. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/sqs.py +35 -23
  18. {stackport-0.4.8 → stackport-0.4.9}/backend/websocket.py +2 -11
  19. {stackport-0.4.8 → stackport-0.4.9}/pyproject.toml +3 -3
  20. {stackport-0.4.8 → stackport-0.4.9/stackport.egg-info}/PKG-INFO +4 -4
  21. {stackport-0.4.8 → stackport-0.4.9}/stackport.egg-info/SOURCES.txt +32 -26
  22. {stackport-0.4.8 → stackport-0.4.9}/stackport.egg-info/requires.txt +2 -2
  23. stackport-0.4.9/tests/test_aws_client_checksums.py +77 -0
  24. {stackport-0.4.8 → stackport-0.4.9}/tests/test_dynamodb_routes.py +281 -9
  25. stackport-0.4.9/tests/test_generic_resources.py +514 -0
  26. {stackport-0.4.8 → stackport-0.4.9}/tests/test_iam_routes.py +100 -181
  27. {stackport-0.4.8 → stackport-0.4.9}/tests/test_logs_routes.py +87 -2
  28. {stackport-0.4.8 → stackport-0.4.9}/tests/test_logs_tail.py +2 -1
  29. {stackport-0.4.8 → stackport-0.4.9}/tests/test_registries.py +32 -1
  30. {stackport-0.4.8 → stackport-0.4.9}/tests/test_request_safety.py +32 -0
  31. {stackport-0.4.8 → stackport-0.4.9}/tests/test_s3_routes.py +137 -152
  32. stackport-0.4.9/tests/test_s3_transfers.py +285 -0
  33. {stackport-0.4.8 → stackport-0.4.9}/tests/test_sqs_routes.py +162 -0
  34. stackport-0.4.9/tests/test_suite_isolation.py +41 -0
  35. stackport-0.4.9/tests/test_tags_roundtrip.py +409 -0
  36. stackport-0.4.8/ui/dist/assets/CloudscapeAbout-ykzJ5x3Y.js → stackport-0.4.9/ui/dist/assets/CloudscapeAbout-C4S2ahmA.js +1 -1
  37. stackport-0.4.8/ui/dist/assets/CloudscapeDashboard-CJt6f1T3.js → stackport-0.4.9/ui/dist/assets/CloudscapeDashboard-CgPMF-G_.js +1 -1
  38. stackport-0.4.8/ui/dist/assets/CloudscapeLearn-l4d6rjUk.js → stackport-0.4.9/ui/dist/assets/CloudscapeLearn-B8fMSiDF.js +1 -1
  39. stackport-0.4.9/ui/dist/assets/CloudscapeResourceBrowser-IhkIJ1Hg.js +19 -0
  40. stackport-0.4.8/ui/dist/assets/CloudscapeSettings-Hg6Upkx0.js → stackport-0.4.9/ui/dist/assets/CloudscapeSettings-D4ZhF-Od.js +1 -1
  41. stackport-0.4.8/ui/dist/assets/CloudscapeShell-S38ixn79.js → stackport-0.4.9/ui/dist/assets/CloudscapeShell-C7gsElS6.js +1 -1
  42. stackport-0.4.8/ui/dist/assets/ServiceStatus-BHSv7SA3.js → stackport-0.4.9/ui/dist/assets/ServiceStatus-Bn8D3Whz.js +1 -1
  43. stackport-0.4.8/ui/dist/assets/StateMachineGraph-DUiGayyA.js → stackport-0.4.9/ui/dist/assets/StateMachineGraph-DBZaV-ye.js +1 -1
  44. stackport-0.4.8/ui/dist/assets/index-v1JyxN8N.js → stackport-0.4.9/ui/dist/assets/index-BIP6RNbD.js +1 -1
  45. stackport-0.4.8/ui/dist/assets/index-ClXofh-m.js → stackport-0.4.9/ui/dist/assets/index-BOjNdz1a.js +1 -1
  46. stackport-0.4.8/ui/dist/assets/index-SWJZXC-H.js → stackport-0.4.9/ui/dist/assets/index-C56KgCLT.js +1 -1
  47. stackport-0.4.8/ui/dist/assets/index-DEgNFpXr.js → stackport-0.4.9/ui/dist/assets/index-R-ZHGMoF.js +1 -1
  48. stackport-0.4.8/ui/dist/assets/index-CTGN6ph7.js → stackport-0.4.9/ui/dist/assets/index-bfBTZg8t.js +7 -7
  49. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/index.html +1 -1
  50. stackport-0.4.8/ui/dist/assets/CloudscapeResourceBrowser-DETT9g8r.js +0 -19
  51. {stackport-0.4.8 → stackport-0.4.9}/LICENSE +0 -0
  52. {stackport-0.4.8 → stackport-0.4.9}/MANIFEST.in +0 -0
  53. {stackport-0.4.8 → stackport-0.4.9}/backend/__init__.py +0 -0
  54. {stackport-0.4.8 → stackport-0.4.9}/backend/aws_emulator.py +0 -0
  55. {stackport-0.4.8 → stackport-0.4.9}/backend/aws_errors.py +0 -0
  56. {stackport-0.4.8 → stackport-0.4.9}/backend/cache.py +0 -0
  57. {stackport-0.4.8 → stackport-0.4.9}/backend/config.py +0 -0
  58. {stackport-0.4.8 → stackport-0.4.9}/backend/connection.py +0 -0
  59. {stackport-0.4.8 → stackport-0.4.9}/backend/emulators.py +0 -0
  60. {stackport-0.4.8 → stackport-0.4.9}/backend/endpoint_store.py +0 -0
  61. {stackport-0.4.8 → stackport-0.4.9}/backend/error_handlers.py +0 -0
  62. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/__init__.py +0 -0
  63. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/cli_setup.py +0 -0
  64. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/content.py +0 -0
  65. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/hotspots.py +0 -0
  66. {stackport-0.4.8 → stackport-0.4.9}/backend/learn/trails/aws-first-steps.json +0 -0
  67. {stackport-0.4.8 → stackport-0.4.9}/backend/learn_store.py +0 -0
  68. {stackport-0.4.8 → stackport-0.4.9}/backend/origins.py +0 -0
  69. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/__init__.py +0 -0
  70. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/common.py +0 -0
  71. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/ec2.py +0 -0
  72. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/endpoints.py +0 -0
  73. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/kms.py +0 -0
  74. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/learn.py +0 -0
  75. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/monitoring.py +0 -0
  76. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/rds.py +0 -0
  77. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/secretsmanager.py +0 -0
  78. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/sns.py +0 -0
  79. {stackport-0.4.8 → stackport-0.4.9}/backend/routes/stepfunctions.py +0 -0
  80. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/__init__.py +0 -0
  81. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/dynamodb.py +0 -0
  82. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/endpoints.py +0 -0
  83. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/lambda_svc.py +0 -0
  84. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/learn.py +0 -0
  85. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/logs.py +0 -0
  86. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/monitoring.py +0 -0
  87. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/s3.py +0 -0
  88. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/secretsmanager.py +0 -0
  89. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/sns.py +0 -0
  90. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/stepfunctions.py +0 -0
  91. {stackport-0.4.8 → stackport-0.4.9}/backend/schemas/tags.py +0 -0
  92. {stackport-0.4.8 → stackport-0.4.9}/setup.cfg +0 -0
  93. {stackport-0.4.8 → stackport-0.4.9}/stackport.egg-info/dependency_links.txt +0 -0
  94. {stackport-0.4.8 → stackport-0.4.9}/stackport.egg-info/entry_points.txt +0 -0
  95. {stackport-0.4.8 → stackport-0.4.9}/stackport.egg-info/top_level.txt +0 -0
  96. {stackport-0.4.8 → stackport-0.4.9}/tests/test_aws_client_tls.py +0 -0
  97. {stackport-0.4.8 → stackport-0.4.9}/tests/test_aws_emulator.py +0 -0
  98. {stackport-0.4.8 → stackport-0.4.9}/tests/test_aws_errors.py +0 -0
  99. {stackport-0.4.8 → stackport-0.4.9}/tests/test_cache.py +0 -0
  100. {stackport-0.4.8 → stackport-0.4.9}/tests/test_cli.py +0 -0
  101. {stackport-0.4.8 → stackport-0.4.9}/tests/test_client.py +0 -0
  102. {stackport-0.4.8 → stackport-0.4.9}/tests/test_config.py +0 -0
  103. {stackport-0.4.8 → stackport-0.4.9}/tests/test_connection.py +0 -0
  104. {stackport-0.4.8 → stackport-0.4.9}/tests/test_create_resources.py +0 -0
  105. {stackport-0.4.8 → stackport-0.4.9}/tests/test_ec2_routes.py +0 -0
  106. {stackport-0.4.8 → stackport-0.4.9}/tests/test_emulators.py +0 -0
  107. {stackport-0.4.8 → stackport-0.4.9}/tests/test_endpoint_store.py +0 -0
  108. {stackport-0.4.8 → stackport-0.4.9}/tests/test_endpoint_store_env.py +0 -0
  109. {stackport-0.4.8 → stackport-0.4.9}/tests/test_endpoints.py +0 -0
  110. {stackport-0.4.8 → stackport-0.4.9}/tests/test_endpoints_tls.py +0 -0
  111. {stackport-0.4.8 → stackport-0.4.9}/tests/test_error_handlers.py +0 -0
  112. {stackport-0.4.8 → stackport-0.4.9}/tests/test_kms_routes.py +0 -0
  113. {stackport-0.4.8 → stackport-0.4.9}/tests/test_lambda_code_download.py +0 -0
  114. {stackport-0.4.8 → stackport-0.4.9}/tests/test_lambda_routes.py +0 -0
  115. {stackport-0.4.8 → stackport-0.4.9}/tests/test_learn_cli_setup.py +0 -0
  116. {stackport-0.4.8 → stackport-0.4.9}/tests/test_learn_content.py +0 -0
  117. {stackport-0.4.8 → stackport-0.4.9}/tests/test_learn_routes.py +0 -0
  118. {stackport-0.4.8 → stackport-0.4.9}/tests/test_learn_store.py +0 -0
  119. {stackport-0.4.8 → stackport-0.4.9}/tests/test_monitoring_routes.py +0 -0
  120. {stackport-0.4.8 → stackport-0.4.9}/tests/test_multi_endpoint.py +0 -0
  121. {stackport-0.4.8 → stackport-0.4.9}/tests/test_rds_routes.py +0 -0
  122. {stackport-0.4.8 → stackport-0.4.9}/tests/test_readonly_middleware.py +0 -0
  123. {stackport-0.4.8 → stackport-0.4.9}/tests/test_routes.py +0 -0
  124. {stackport-0.4.8 → stackport-0.4.9}/tests/test_s3_upload_limit_env.py +0 -0
  125. {stackport-0.4.8 → stackport-0.4.9}/tests/test_secretsmanager_routes.py +0 -0
  126. {stackport-0.4.8 → stackport-0.4.9}/tests/test_sns_routes.py +0 -0
  127. {stackport-0.4.8 → stackport-0.4.9}/tests/test_stats_probe.py +0 -0
  128. {stackport-0.4.8 → stackport-0.4.9}/tests/test_tags_routes.py +0 -0
  129. {stackport-0.4.8 → stackport-0.4.9}/tests/test_websocket.py +0 -0
  130. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/CloudscapeResourceBrowser-BR6m7Ajg.css +0 -0
  131. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/CloudscapeSettings-DI0pOcAA.css +0 -0
  132. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/CloudscapeShell-oIMuVycJ.css +0 -0
  133. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/ServiceStatus-DNBdeKId.css +0 -0
  134. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/index-DKIdXFX0.css +0 -0
  135. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/index-D__irddE.css +0 -0
  136. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/index-DoY0HscW.css +0 -0
  137. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/index-RwoKZF4y.css +0 -0
  138. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  139. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/assets/utils-Dm4tJbJR.js +0 -0
  140. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/acm.svg +0 -0
  141. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/apigateway.svg +0 -0
  142. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/appsync.svg +0 -0
  143. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/athena.svg +0 -0
  144. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/cloudformation.svg +0 -0
  145. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/cloudfront.svg +0 -0
  146. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  147. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/dynamodb.svg +0 -0
  148. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/ec2.svg +0 -0
  149. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/ecr.svg +0 -0
  150. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/ecs.svg +0 -0
  151. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/elasticache.svg +0 -0
  152. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  153. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  154. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  155. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/events.svg +0 -0
  156. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/firehose.svg +0 -0
  157. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/glue.svg +0 -0
  158. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/iam.svg +0 -0
  159. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/kinesis.svg +0 -0
  160. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/kms.svg +0 -0
  161. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/lambda.svg +0 -0
  162. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/logs.svg +0 -0
  163. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/monitoring.svg +0 -0
  164. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/rds.svg +0 -0
  165. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/route53.svg +0 -0
  166. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/s3.svg +0 -0
  167. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  168. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/ses.svg +0 -0
  169. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/sns.svg +0 -0
  170. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/sqs.svg +0 -0
  171. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/ssm.svg +0 -0
  172. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  173. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/aws-icons/wafv2.svg +0 -0
  174. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/favicon.png +0 -0
  175. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/favicon.svg +0 -0
  176. {stackport-0.4.8 → stackport-0.4.9}/ui/dist/stackport-logo.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.4.8
3
+ Version: 0.4.9
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: fastapi>=0.115.0
25
25
  Requires-Dist: uvicorn>=0.30.0
26
- Requires-Dist: boto3>=1.35.0
26
+ Requires-Dist: boto3>=1.36.0
27
27
  Requires-Dist: click>=8.0
28
28
  Requires-Dist: websockets>=12.0
29
29
  Requires-Dist: python-multipart>=0.0.26
@@ -31,7 +31,7 @@ Provides-Extra: dev
31
31
  Requires-Dist: pytest>=8.0; extra == "dev"
32
32
  Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
33
33
  Requires-Dist: httpx>=0.27.0; extra == "dev"
34
- Requires-Dist: moto[dynamodb,iam,lambda,s3,sqs]>=5.0; extra == "dev"
34
+ Requires-Dist: moto[apigateway,apigatewayv2,cognitoidp,dynamodb,glue,iam,lambda,s3,sqs]>=5.0; extra == "dev"
35
35
  Dynamic: license-file
36
36
 
37
37
  <p align="center">
@@ -249,7 +249,7 @@ You can test a connection before saving it with the "Test Connection" button in
249
249
  | Service | Browse | Write operations |
250
250
  |---------|--------|------------------|
251
251
  | **S3** | Buckets, objects, folder navigation, search | Create buckets and folders, upload, download, delete, batch delete; edit versioning, lifecycle, notifications, CORS, tags |
252
- | **DynamoDB** | Tables, schema, items, scan and query | Create tables, add, edit and delete items, batch write |
252
+ | **DynamoDB** | Tables, schema, items, scan and query, live item count | Create tables, add, edit and delete items, batch write |
253
253
  | **Lambda** | Functions, config, aliases, versions, event sources | Invoke with a JSON payload, edit configuration, download code |
254
254
  | **SQS** | Queues, messages, attributes | Create and delete queues, send (single or batch), receive, delete messages, purge, edit attributes and redrive policy |
255
255
  | **SNS** | Topics, subscriptions | Create and delete topics, subscribe, unsubscribe, publish |
@@ -213,7 +213,7 @@ You can test a connection before saving it with the "Test Connection" button in
213
213
  | Service | Browse | Write operations |
214
214
  |---------|--------|------------------|
215
215
  | **S3** | Buckets, objects, folder navigation, search | Create buckets and folders, upload, download, delete, batch delete; edit versioning, lifecycle, notifications, CORS, tags |
216
- | **DynamoDB** | Tables, schema, items, scan and query | Create tables, add, edit and delete items, batch write |
216
+ | **DynamoDB** | Tables, schema, items, scan and query, live item count | Create tables, add, edit and delete items, batch write |
217
217
  | **Lambda** | Functions, config, aliases, versions, event sources | Invoke with a JSON payload, edit configuration, download code |
218
218
  | **SQS** | Queues, messages, attributes | Create and delete queues, send (single or batch), receive, delete messages, purge, edit attributes and redrive policy |
219
219
  | **SNS** | Topics, subscriptions | Create and delete topics, subscribe, unsubscribe, publish |
@@ -26,10 +26,18 @@ _UNSET = object()
26
26
  # dozens of services. Locally nothing is throttled, a refused connection or a
27
27
  # rejected certificate won't fix itself on retry, and the UI polls again in
28
28
  # seconds, so fail fast instead. STACKPORT_PROBE_TIMEOUT bounds the connect.
29
+ # - Flexible checksums (CRC32 on uploads, checked on downloads) only where an API
30
+ # requires them, as before botocore 1.36. Emulators keep them unevenly: moto
31
+ # returns a multipart object's checksum of part checksums as if it covered the
32
+ # whole object, so a download of it failed its check. Without asking for them,
33
+ # StackPort's multipart uploads don't leave such objects behind for other
34
+ # clients either.
29
35
  _LOCAL_ENDPOINT_CONFIG = Config(
30
36
  inject_host_prefix=False,
31
37
  connect_timeout=STACKPORT_PROBE_TIMEOUT,
32
38
  retries={"mode": "standard", "total_max_attempts": 1},
39
+ request_checksum_calculation="when_required",
40
+ response_checksum_validation="when_required",
33
41
  )
34
42
 
35
43
  # Last stat stamp seen per AWS Emulator files directory, to notice restarts.
@@ -22,12 +22,13 @@ from backend.config import (
22
22
  from backend.connection import EndpointInfo, InvalidEndpointUrl
23
23
  from backend.routes.common import UnknownEndpoint, endpoint_info_for
24
24
  from backend.routes.resources import (
25
- DESCRIBE_REGISTRY,
26
25
  _PREFERRED_ID_FIELD,
26
+ UnknownResourceType,
27
27
  _extract_id,
28
28
  _serialize,
29
+ describe_resource,
29
30
  )
30
- from backend.routes.stats import SERVICE_REGISTRY, _METHOD_KWARGS, _count_items, _probe_service
31
+ from backend.routes.stats import SERVICE_REGISTRY, _probe_service, list_items
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
@@ -149,14 +150,7 @@ def list(service, endpoint, region, output):
149
150
  for resource_type, boto3_service, method_name, response_key in registry_entries:
150
151
  try:
151
152
  client = get_client(boto3_service, **client_kwargs)
152
- method = getattr(client, method_name)
153
- kwargs = _METHOD_KWARGS.get((boto3_service, method_name), {})
154
- resp = method(**kwargs)
155
- items = resp.get(response_key, [])
156
- # Handle nested structures (e.g., cloudfront DistributionList.Items)
157
- if isinstance(items, dict) and "Items" in items:
158
- items = items.get("Items", []) or []
159
- resources[resource_type] = items
153
+ resources[resource_type] = list_items(client, boto3_service, method_name, response_key)
160
154
  except Exception as exc:
161
155
  logger.debug("Failed to list %s/%s: %s", service, resource_type, exc, exc_info=True)
162
156
  resources[resource_type] = []
@@ -207,61 +201,16 @@ def describe(service, resource_type, resource_id, endpoint, region, output):
207
201
  """Describe a specific resource."""
208
202
  client_kwargs = _client_kwargs(endpoint, region)
209
203
 
210
- # Special case for WAFv2
211
- if (service, resource_type) == ("wafv2", "web_acls"):
212
- try:
213
- client = get_client("wafv2", **client_kwargs)
214
- acls = client.list_web_acls(Scope="REGIONAL").get("WebACLs", [])
215
- match = next((a for a in acls if a.get("Name") == resource_id), None)
216
- if not match:
217
- click.echo(f"Error: Web ACL '{resource_id}' not found", err=True)
218
- sys.exit(1)
219
- resp = client.get_web_acl(Name=resource_id, Scope="REGIONAL", Id=match["Id"])
220
- resp.pop("ResponseMetadata", None)
221
- detail = _serialize(resp.get("WebACL", resp))
222
- except Exception as exc:
223
- click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
224
- sys.exit(1)
225
- else:
226
- lookup = DESCRIBE_REGISTRY.get((service, resource_type))
227
- if not lookup:
228
- click.echo(f"Error: No detail lookup registered for {service}/{resource_type}", err=True)
229
- sys.exit(1)
230
-
231
- boto3_service, method_name, id_param, response_key = lookup
232
-
233
- # Some APIs take list parameters
234
- _LIST_PARAMS = {
235
- "InstanceIds",
236
- "VpcIds",
237
- "SubnetIds",
238
- "GroupIds",
239
- "VolumeIds",
240
- "repositoryNames",
241
- "clusters",
242
- "AlarmNames",
243
- "LoadBalancerArns",
244
- }
245
-
246
- try:
247
- client = get_client(boto3_service, **client_kwargs)
248
- method = getattr(client, method_name)
249
- if id_param in _LIST_PARAMS:
250
- resp = method(**{id_param: [resource_id]})
251
- else:
252
- resp = method(**{id_param: resource_id})
253
-
254
- resp.pop("ResponseMetadata", None)
255
-
256
- if response_key is not None:
257
- detail = resp.get(response_key, resp)
258
- else:
259
- detail = resp
260
-
261
- detail = _serialize(detail)
262
- except Exception as exc:
263
- click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
264
- sys.exit(1)
204
+ try:
205
+ detail = describe_resource(
206
+ lambda boto3_service: get_client(boto3_service, **client_kwargs), service, resource_type, resource_id
207
+ )
208
+ except UnknownResourceType as exc:
209
+ click.echo(f"Error: {exc}", err=True)
210
+ sys.exit(1)
211
+ except Exception as exc:
212
+ click.echo(f"Error describing {service}/{resource_type}/{resource_id}: {exc}", err=True)
213
+ sys.exit(1)
265
214
 
266
215
  if output == "json":
267
216
  click.echo(json.dumps(detail, indent=2))
@@ -0,0 +1,64 @@
1
+ """Passing a download from an endpoint on to the browser: S3 objects, Lambda code packages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterable, Mapping
7
+ from typing import Protocol
8
+ from urllib.parse import quote
9
+
10
+ from fastapi.responses import StreamingResponse
11
+ from starlette.types import Receive, Scope, Send
12
+
13
+ _PLAIN_FILENAME = re.compile(r"[A-Za-z0-9._-]+")
14
+
15
+ # How much of the upstream body to read for each chunk sent on. Reading a sync
16
+ # body costs a hop through the thread pool per chunk, so tiny chunks make a
17
+ # download crawl; at this size, relaying keeps pace with reading directly.
18
+ RELAY_CHUNK_BYTES = 64 * 1024
19
+
20
+
21
+ class Closeable(Protocol):
22
+ def close(self) -> None: ...
23
+
24
+
25
+ def attachment(filename: str) -> str:
26
+ """A Content-Disposition value that saves the download as ``filename`` (RFC 6266).
27
+
28
+ A name outside a conservative ASCII set gets an underscored fallback plus the
29
+ exact name, percent-encoded as UTF-8, in ``filename*`` (RFC 8187), so quotes,
30
+ backslashes, semicolons, line breaks and characters outside Latin-1 can't
31
+ break the header. The fallback replaces quotes and backslashes rather than
32
+ escaping them, since some browsers don't unescape them (RFC 6266, appendix D).
33
+ """
34
+ if _PLAIN_FILENAME.fullmatch(filename):
35
+ return f'attachment; filename="{filename}"'
36
+ fallback = re.sub(r"[^A-Za-z0-9._-]", "_", filename)
37
+ return f"attachment; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename, safe='')}"
38
+
39
+
40
+ class RelayResponse(StreamingResponse):
41
+ """Streams a body read from an open upstream response, and closes the upstream however the response ends.
42
+
43
+ When the client goes away, Starlette stops iterating the body without
44
+ closing it, so the body iterator can't be the one that closes the upstream.
45
+ An error while reading the upstream propagates, so the server drops the
46
+ connection instead of finishing a cut-off download that would look complete.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ content: Iterable[bytes],
52
+ *,
53
+ upstream: Closeable,
54
+ media_type: str,
55
+ headers: Mapping[str, str],
56
+ ):
57
+ super().__init__(content, media_type=media_type, headers=headers)
58
+ self._upstream = upstream
59
+
60
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
61
+ try:
62
+ await super().__call__(scope, receive, send)
63
+ finally:
64
+ self._upstream.close()
@@ -98,7 +98,7 @@ def _dynamodb_item_count(ep: EndpointInfo, params: dict) -> dict:
98
98
 
99
99
  def _resource_exists(ep: EndpointInfo, params: dict) -> dict:
100
100
  """Generic existence check driven by SERVICE_REGISTRY (no cache)."""
101
- from backend.routes.stats import _METHOD_KWARGS, SERVICE_REGISTRY
101
+ from backend.routes.stats import SERVICE_REGISTRY, list_items
102
102
 
103
103
  service = params["service"]
104
104
  resource_type = params["resource_type"]
@@ -108,12 +108,7 @@ def _resource_exists(ep: EndpointInfo, params: dict) -> dict:
108
108
  if rtype != resource_type:
109
109
  continue
110
110
  client = get_client(boto_service, **ep.client_kwargs())
111
- kwargs = _METHOD_KWARGS.get((boto_service, method), {})
112
- response = getattr(client, method)(**kwargs)
113
- items = response.get(response_key, [])
114
- if isinstance(items, dict) and "Items" in items:
115
- items = items["Items"]
116
- for item in items:
111
+ for item in list_items(client, boto_service, method, response_key):
117
112
  if isinstance(item, str) and target in item:
118
113
  return {"passed": True, "message": f"Found {resource_type[:-1] if resource_type.endswith('s') else resource_type} '{target}'."}
119
114
  if isinstance(item, dict) and any(target in str(v) for v in item.values() if isinstance(v, str)):
@@ -8,6 +8,7 @@ from fastapi import FastAPI, Request, WebSocket
8
8
  from fastapi.middleware.cors import CORSMiddleware
9
9
  from fastapi.responses import FileResponse, JSONResponse
10
10
  from fastapi.staticfiles import StaticFiles
11
+ from starlette.convertors import PathConvertor, register_url_convertor
11
12
  from starlette.types import ASGIApp, Receive, Scope, Send
12
13
 
13
14
  from botocore.exceptions import (
@@ -175,14 +176,27 @@ async def ws_logs_tail(websocket: WebSocket):
175
176
  await logs_tail_endpoint(websocket)
176
177
 
177
178
 
178
- # Serve UI static files — mount assets under /assets, SPA fallback for everything else
179
+ class AppPagePath(PathConvertor):
180
+ """A path the SPA fallback answers: anything except /api, /ws and paths under them.
181
+
182
+ Those never reach the fallback, so an unknown API path gets the same JSON
183
+ 404 (or 405 for a known path and another method) as when the UI isn't
184
+ built, rather than the app page with a 200.
185
+ """
186
+
187
+ regex = r"(?!(?:api|ws)(?:/|$)).*"
188
+
189
+
190
+ register_url_convertor("app_page", AppPagePath())
191
+
192
+ # Serve UI static files — mount assets under /assets, SPA fallback for every other page
179
193
  ui_dist = os.path.join(os.path.dirname(__file__), "..", "ui", "dist")
180
194
  if os.path.isdir(ui_dist):
181
195
  app.mount("/assets", StaticFiles(directory=os.path.join(ui_dist, "assets")), name="assets")
182
196
 
183
197
  ui_root = os.path.realpath(ui_dist)
184
198
 
185
- @app.get("/{path:path}")
199
+ @app.get("/{path:app_page}")
186
200
  def spa_fallback(path: str):
187
201
  # Serve a file from ui/dist when the path names one inside it. The path
188
202
  # comes from the URL, where %2F decodes to "/", so resolve it and check