localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__py3-none-any.whl

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 (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
@@ -38,14 +38,14 @@ def patch_urllib3_connection_pool(**constructor_kwargs):
38
38
  class MyHTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
39
39
  def __init__(self, *args, **kwargs):
40
40
  kwargs.update(constructor_kwargs)
41
- super(MyHTTPSConnectionPool, self).__init__(*args, **kwargs)
41
+ super().__init__(*args, **kwargs)
42
42
 
43
43
  poolmanager.pool_classes_by_scheme["https"] = MyHTTPSConnectionPool
44
44
 
45
45
  class MyHTTPConnectionPool(connectionpool.HTTPConnectionPool):
46
46
  def __init__(self, *args, **kwargs):
47
47
  kwargs.update(constructor_kwargs)
48
- super(MyHTTPConnectionPool, self).__init__(*args, **kwargs)
48
+ super().__init__(*args, **kwargs)
49
49
 
50
50
  poolmanager.pool_classes_by_scheme["http"] = MyHTTPConnectionPool
51
51
  except Exception:
@@ -367,10 +367,7 @@ def resolve_references(data: dict, rest_api_id, allow_recursive=True) -> dict:
367
367
 
368
368
  def path_based_url(api_id: str, stage_name: str, path: str) -> str:
369
369
  """Return URL for inbound API gateway for given API ID, stage name, and path"""
370
- pattern = "%s/restapis/{api_id}/{stage_name}/%s{path}" % (
371
- config.external_service_url(),
372
- PATH_USER_REQUEST,
373
- )
370
+ pattern = f"{config.external_service_url()}/restapis/{{api_id}}/{{stage_name}}/{PATH_USER_REQUEST}{{path}}"
374
371
  return pattern.format(api_id=api_id, stage_name=stage_name, path=path)
375
372
 
376
373
 
@@ -37,7 +37,7 @@ PATH_REGEX_TEST_INVOKE_API = r"^\/restapis\/([A-Za-z0-9_\-]+)\/resources\/([A-Za
37
37
 
38
38
  # regex path pattern for user requests, handles stages like $default
39
39
  PATH_REGEX_USER_REQUEST = (
40
- r"^/restapis/([A-Za-z0-9_\\-]+)(?:/([A-Za-z0-9\_($|%%24)\\-]+))?/%s/(.*)$" % PATH_USER_REQUEST
40
+ rf"^/restapis/([A-Za-z0-9_\\-]+)(?:/([A-Za-z0-9\_($|%24)\\-]+))?/{PATH_USER_REQUEST}/(.*)$"
41
41
  )
42
42
  # URL pattern for invocations
43
43
  HOST_REGEX_EXECUTE_API = r"(?:.*://)?([a-zA-Z0-9]+)(?:(-vpce-[^.]+))?\.execute-api\.(.*)"
@@ -375,12 +375,12 @@ def get_apigateway_path_for_resource(
375
375
  path_part = target_resource.get("pathPart", "")
376
376
  if path_suffix:
377
377
  if path_part:
378
- path_suffix = "%s/%s" % (path_part, path_suffix)
378
+ path_suffix = f"{path_part}/{path_suffix}"
379
379
  else:
380
380
  path_suffix = path_part
381
381
  parent_id = target_resource.get("parentId")
382
382
  if not parent_id:
383
- return "/%s" % path_suffix
383
+ return f"/{path_suffix}"
384
384
  return get_apigateway_path_for_resource(
385
385
  api_id,
386
386
  parent_id,
@@ -419,7 +419,7 @@ def get_resource_for_path(
419
419
  for api_path, details in path_map.items():
420
420
  api_path_regex = re.sub(r"{[^+]+\+}", r"[^\?#]+", api_path)
421
421
  api_path_regex = re.sub(r"{[^}]+}", r"[^/]+", api_path_regex)
422
- if re.match(r"^%s$" % api_path_regex, path):
422
+ if re.match(rf"^{api_path_regex}$", path):
423
423
  matches.append((api_path, details))
424
424
 
425
425
  # if there are no matches, it's not worth to proceed, bail here!
@@ -508,8 +508,7 @@ def connect_api_gateway_to_sqs(gateway_name, stage_name, queue_arn, path, accoun
508
508
  "integrations": [
509
509
  {
510
510
  "type": "AWS",
511
- "uri": "arn:%s:apigateway:%s:sqs:path/%s/%s"
512
- % (partition, sqs_region, sqs_account, queue_name),
511
+ "uri": f"arn:{partition}:apigateway:{sqs_region}:sqs:path/{sqs_account}/{queue_name}",
513
512
  "requestTemplates": {"application/json": template},
514
513
  "requestParameters": {
515
514
  "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'"
@@ -660,11 +659,11 @@ def set_api_id_stage_invocation_path(
660
659
  if path_match:
661
660
  api_id = path_match.group(1)
662
661
  stage = path_match.group(2)
663
- relative_path_w_query_params = "/%s" % path_match.group(3)
662
+ relative_path_w_query_params = f"/{path_match.group(3)}"
664
663
  elif host_match:
665
664
  api_id = extract_api_id_from_hostname_in_url(host_header)
666
665
  stage = path.strip("/").split("/")[0]
667
- relative_path_w_query_params = "/%s" % path.lstrip("/").partition("/")[2]
666
+ relative_path_w_query_params = "/{}".format(path.lstrip("/").partition("/")[2])
668
667
  elif test_invoke_match:
669
668
  stage = invocation_context.stage
670
669
  api_id = invocation_context.api_id
@@ -783,7 +783,7 @@ class HTTPIntegration(BackendIntegration):
783
783
  instances = client.list_instances(ServiceId=service_id)["Instances"]
784
784
  instance = (instances or [None])[0]
785
785
  if instance and instance.get("Id"):
786
- uri = "http://%s/%s" % (instance["Id"], invocation_path.lstrip("/"))
786
+ uri = "http://{}/{}".format(instance["Id"], invocation_path.lstrip("/"))
787
787
 
788
788
  # apply custom request template
789
789
  invocation_context.context = get_event_request_context(invocation_context)
@@ -977,8 +977,9 @@ class StepFunctionIntegration(BackendIntegration):
977
977
  headers={"Content-Type": APPLICATION_JSON},
978
978
  data=json.dumps(
979
979
  {
980
- "message": "StepFunctions execution %s failed with status '%s'"
981
- % (result["executionArn"], result_status)
980
+ "message": "StepFunctions execution {} failed with status '{}'".format(
981
+ result["executionArn"], result_status
982
+ )
982
983
  }
983
984
  ),
984
985
  )
@@ -142,8 +142,9 @@ class RequestValidator:
142
142
  # try to get the resolved model first
143
143
  resolved_schema = model_resolver.get_resolved_model()
144
144
  if not resolved_schema:
145
- LOG.exception(
146
- "An exception occurred while trying to validate the request: could not find the model"
145
+ LOG.error(
146
+ "An exception occurred while trying to validate the request: could not find the model",
147
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
147
148
  )
148
149
  return False
149
150
 
@@ -279,7 +280,7 @@ def invoke_rest_api(invocation_context: ApiInvocationContext):
279
280
 
280
281
  extracted_path, resource = get_target_resource_details(invocation_context)
281
282
  if not resource:
282
- return make_error_response("Unable to find path %s" % invocation_context.path, 404)
283
+ return make_error_response(f"Unable to find path {invocation_context.path}", 404)
283
284
 
284
285
  # validate request
285
286
  validator = RequestValidator(invocation_context)
@@ -306,7 +307,7 @@ def invoke_rest_api(invocation_context: ApiInvocationContext):
306
307
  # default to returning CORS headers if this is an OPTIONS request
307
308
  return get_cors_response(headers)
308
309
  return make_error_response(
309
- "Unable to find integration for: %s %s (%s)" % (method, invocation_path, raw_path),
310
+ f"Unable to find integration for: {method} {invocation_path} ({raw_path})",
310
311
  404,
311
312
  )
312
313
 
@@ -334,7 +335,7 @@ def invoke_rest_api_integration(invocation_context: ApiInvocationContext):
334
335
  return e.to_response()
335
336
  except Exception as e:
336
337
  msg = f"Error invoking integration for API Gateway ID '{invocation_context.api_id}': {e}"
337
- LOG.exception(msg)
338
+ LOG.error(msg, exc_info=LOG.isEnabledFor(logging.DEBUG))
338
339
  return make_error_response(msg, 400)
339
340
 
340
341
 
@@ -323,8 +323,18 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
323
323
  tags: MapOfStringToString = None,
324
324
  **kwargs,
325
325
  ) -> ApiKey:
326
+ if name and len(name) > 1024:
327
+ raise BadRequestException("Invalid API Key name, can be at most 1024 characters.")
328
+ if value:
329
+ if len(value) > 128:
330
+ raise BadRequestException("API Key value exceeds maximum size of 128 characters")
331
+ elif len(value) < 20:
332
+ raise BadRequestException("API Key value should be at least 20 characters")
333
+ if description and len(description) > 125000:
334
+ raise BadRequestException("Invalid API Key description specified.")
326
335
  api_key = call_moto(context)
327
-
336
+ if name == "":
337
+ api_key.pop("name", None)
328
338
  # transform array of stage keys [{'restApiId': '0iscapk09u', 'stageName': 'dev'}] into
329
339
  # array of strings ['0iscapk09u/dev']
330
340
  stage_keys = api_key.get("stageKeys", [])
@@ -665,66 +675,6 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
665
675
  parent_id = moto_resource.parent_id
666
676
  api_resources[parent_id].remove(resource_id)
667
677
 
668
- def update_integration_response(
669
- self,
670
- context: RequestContext,
671
- rest_api_id: String,
672
- resource_id: String,
673
- http_method: String,
674
- status_code: StatusCode,
675
- patch_operations: ListOfPatchOperation = None,
676
- **kwargs,
677
- ) -> IntegrationResponse:
678
- # XXX: THIS IS NOT A COMPLETE IMPLEMENTATION, just the minimum required to get tests going
679
- # TODO: validate patch operations
680
-
681
- moto_rest_api = get_moto_rest_api(context, rest_api_id)
682
- moto_resource = moto_rest_api.resources.get(resource_id)
683
- if not moto_resource:
684
- raise NotFoundException("Invalid Resource identifier specified")
685
-
686
- moto_method = moto_resource.resource_methods.get(http_method)
687
- if not moto_method:
688
- raise NotFoundException("Invalid Method identifier specified")
689
-
690
- integration_response = moto_method.method_integration.integration_responses.get(status_code)
691
- if not integration_response:
692
- raise NotFoundException("Invalid Integration Response identifier specified")
693
-
694
- for patch_operation in patch_operations:
695
- op = patch_operation.get("op")
696
- path = patch_operation.get("path")
697
-
698
- # for path "/responseTemplates/application~1json"
699
- if "/responseTemplates" in path:
700
- integration_response.response_templates = (
701
- integration_response.response_templates or {}
702
- )
703
- value = patch_operation.get("value")
704
- if not isinstance(value, str):
705
- raise BadRequestException(
706
- f"Invalid patch value '{value}' specified for op '{op}'. Must be a string"
707
- )
708
- param = path.removeprefix("/responseTemplates/")
709
- param = param.replace("~1", "/")
710
- if op == "remove":
711
- integration_response.response_templates.pop(param)
712
- elif op in ("add", "replace"):
713
- integration_response.response_templates[param] = value
714
-
715
- elif "/contentHandling" in path and op == "replace":
716
- integration_response.content_handling = patch_operation.get("value")
717
-
718
- elif "/selectionPattern" in path and op == "replace":
719
- integration_response.selection_pattern = patch_operation.get("value")
720
-
721
- response: IntegrationResponse = integration_response.to_json()
722
- # in case it's empty, we still want to pass it on as ""
723
- # TODO: add a test case for this
724
- response["selectionPattern"] = integration_response.selection_pattern
725
-
726
- return response
727
-
728
678
  def update_resource(
729
679
  self,
730
680
  context: RequestContext,
@@ -757,6 +707,9 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
757
707
  f"Invalid patch path '{path}' specified for op '{op}'. Please choose supported operations"
758
708
  )
759
709
 
710
+ if moto_resource.parent_id is None:
711
+ raise BadRequestException(f"Root resource cannot update its {path.strip('/')}.")
712
+
760
713
  if path == "/parentId":
761
714
  value = patch_operation.get("value")
762
715
  future_parent_resource = moto_rest_api.resources.get(value)
@@ -793,7 +746,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
793
746
  api_resources.pop(current_parent_id)
794
747
 
795
748
  # add it to the new parent children
796
- future_sibling_resources = api_resources[moto_resource.parent_id]
749
+ future_sibling_resources = api_resources.setdefault(moto_resource.parent_id, [])
797
750
  future_sibling_resources.append(resource_id)
798
751
 
799
752
  response = moto_resource.to_dict()
@@ -2111,6 +2064,20 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2111
2064
  for integration_response in integration_responses.values():
2112
2065
  remove_empty_attributes_from_integration_response(integration_response)
2113
2066
 
2067
+ if response.get("connectionType") == "VPC_LINK":
2068
+ # FIXME: this is hacky to workaround moto not saving the VPC Link `connectionId`
2069
+ # only do this internal check of Moto if the integration is of VPC_LINK type
2070
+ moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id)
2071
+ try:
2072
+ method = moto_rest_api.resources[resource_id].resource_methods[http_method]
2073
+ integration = method.method_integration
2074
+ if connection_id := getattr(integration, "connection_id", None):
2075
+ response["connectionId"] = connection_id
2076
+
2077
+ except (AttributeError, KeyError):
2078
+ # this error should have been caught by `call_moto`
2079
+ pass
2080
+
2114
2081
  return response
2115
2082
 
2116
2083
  def put_integration(
@@ -2165,6 +2132,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2165
2132
  moto_request.setdefault("timeoutInMillis", 29000)
2166
2133
  if integration_type in (IntegrationType.HTTP, IntegrationType.HTTP_PROXY):
2167
2134
  moto_request.setdefault("connectionType", ConnectionType.INTERNET)
2135
+
2168
2136
  response = call_moto_with_request(context, moto_request)
2169
2137
  remove_empty_attributes_from_integration(integration=response)
2170
2138
 
@@ -2172,6 +2140,13 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2172
2140
  if integration_type == "MOCK":
2173
2141
  response.pop("uri", None)
2174
2142
 
2143
+ # TODO: moto does not save the connection_id
2144
+ elif moto_request.get("connectionType") == "VPC_LINK":
2145
+ connection_id = moto_request.get("connectionId", "")
2146
+ # attach the connection id to the moto object
2147
+ method.method_integration.connection_id = connection_id
2148
+ response["connectionId"] = connection_id
2149
+
2175
2150
  return response
2176
2151
 
2177
2152
  def update_integration(
@@ -2193,6 +2168,7 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2193
2168
  raise NotFoundException("Invalid Integration identifier specified")
2194
2169
 
2195
2170
  integration = method.method_integration
2171
+ # TODO: validate the patch operations
2196
2172
  patch_api_gateway_entity(integration, patch_operations)
2197
2173
 
2198
2174
  # fix data types
@@ -2201,8 +2177,12 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2201
2177
  if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"):
2202
2178
  integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification)
2203
2179
 
2204
- integration_dict: Integration = integration.to_json()
2205
- return integration_dict
2180
+ response: Integration = integration.to_json()
2181
+
2182
+ if connection_id := getattr(integration, "connection_id", None):
2183
+ response["connectionId"] = connection_id
2184
+
2185
+ return response
2206
2186
 
2207
2187
  def delete_integration(
2208
2188
  self,
@@ -2307,6 +2287,93 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2307
2287
 
2308
2288
  return response
2309
2289
 
2290
+ def update_integration_response(
2291
+ self,
2292
+ context: RequestContext,
2293
+ rest_api_id: String,
2294
+ resource_id: String,
2295
+ http_method: String,
2296
+ status_code: StatusCode,
2297
+ patch_operations: ListOfPatchOperation = None,
2298
+ **kwargs,
2299
+ ) -> IntegrationResponse:
2300
+ # XXX: THIS IS NOT A COMPLETE IMPLEMENTATION, just the minimum required to get tests going
2301
+ # TODO: validate patch operations
2302
+
2303
+ moto_rest_api = get_moto_rest_api(context, rest_api_id)
2304
+ moto_resource = moto_rest_api.resources.get(resource_id)
2305
+ if not moto_resource:
2306
+ raise NotFoundException("Invalid Resource identifier specified")
2307
+
2308
+ moto_method = moto_resource.resource_methods.get(http_method)
2309
+ if not moto_method:
2310
+ raise NotFoundException("Invalid Method identifier specified")
2311
+
2312
+ integration_response = moto_method.method_integration.integration_responses.get(status_code)
2313
+ if not integration_response:
2314
+ raise NotFoundException("Invalid Integration Response identifier specified")
2315
+
2316
+ for patch_operation in patch_operations:
2317
+ op = patch_operation.get("op")
2318
+ path = patch_operation.get("path")
2319
+
2320
+ # for path "/responseTemplates/application~1json"
2321
+ if "/responseTemplates" in path:
2322
+ integration_response.response_templates = (
2323
+ integration_response.response_templates or {}
2324
+ )
2325
+ value = patch_operation.get("value")
2326
+ if not isinstance(value, str):
2327
+ raise BadRequestException(
2328
+ f"Invalid patch value '{value}' specified for op '{op}'. Must be a string"
2329
+ )
2330
+ param = path.removeprefix("/responseTemplates/")
2331
+ param = param.replace("~1", "/")
2332
+ if op == "remove":
2333
+ integration_response.response_templates.pop(param)
2334
+ elif op in ("add", "replace"):
2335
+ integration_response.response_templates[param] = value
2336
+
2337
+ elif "/contentHandling" in path and op == "replace":
2338
+ integration_response.content_handling = patch_operation.get("value")
2339
+
2340
+ elif "/selectionPattern" in path and op == "replace":
2341
+ integration_response.selection_pattern = patch_operation.get("value")
2342
+
2343
+ response: IntegrationResponse = integration_response.to_json()
2344
+ # in case it's empty, we still want to pass it on as ""
2345
+ # TODO: add a test case for this
2346
+ response["selectionPattern"] = integration_response.selection_pattern
2347
+
2348
+ return response
2349
+
2350
+ def delete_integration_response(
2351
+ self,
2352
+ context: RequestContext,
2353
+ rest_api_id: String,
2354
+ resource_id: String,
2355
+ http_method: String,
2356
+ status_code: StatusCode,
2357
+ **kwargs,
2358
+ ) -> None:
2359
+ moto_backend = apigw_models.apigateway_backends[context.account_id][context.region]
2360
+ moto_rest_api = moto_backend.apis.get(rest_api_id)
2361
+ if not moto_rest_api:
2362
+ raise NotFoundException("Invalid Resource identifier specified")
2363
+
2364
+ if not (moto_resource := moto_rest_api.resources.get(resource_id)):
2365
+ raise NotFoundException("Invalid Resource identifier specified")
2366
+
2367
+ if not (moto_method := moto_resource.resource_methods.get(http_method)):
2368
+ raise NotFoundException("Invalid Integration identifier specified")
2369
+
2370
+ if not moto_method.method_integration:
2371
+ raise NotFoundException("Invalid Integration identifier specified")
2372
+ if not (
2373
+ integration_responses := moto_method.method_integration.integration_responses
2374
+ ) or not integration_responses.pop(status_code, None):
2375
+ raise NotFoundException("Invalid Response status code specified")
2376
+
2310
2377
  def get_export(
2311
2378
  self,
2312
2379
  context: RequestContext,
@@ -2363,6 +2430,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2363
2430
  for api_key in api_keys:
2364
2431
  api_key.pop("value")
2365
2432
 
2433
+ if limit is not None:
2434
+ if limit < 1 or limit > 500:
2435
+ limit = None
2436
+
2366
2437
  item_list = PaginatedList(api_keys)
2367
2438
 
2368
2439
  def token_generator(item):
@@ -2387,6 +2458,14 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
2387
2458
  patch_operations: ListOfPatchOperation = None,
2388
2459
  **kwargs,
2389
2460
  ) -> ApiKey:
2461
+ for patch_op in patch_operations:
2462
+ if patch_op["path"] not in ("/description", "/enabled", "/name", "/customerId"):
2463
+ raise BadRequestException(
2464
+ f"Invalid patch path '{patch_op['path']}' specified for op '{patch_op['op']}'. Must be one of: [/description, /enabled, /name, /customerId]"
2465
+ )
2466
+
2467
+ if patch_op["path"] == "/description" and len(patch_op["value"]) > 125000:
2468
+ raise BadRequestException("Invalid API Key description specified.")
2390
2469
  response: ApiKey = call_moto(context)
2391
2470
  if "value" in response:
2392
2471
  response.pop("value", None)
@@ -2947,6 +3026,7 @@ def create_custom_context(
2947
3026
  ctx = create_aws_request_context(
2948
3027
  service_name=context.service.service_name,
2949
3028
  action=action,
3029
+ protocol=context.service.protocol,
2950
3030
  parameters=parameters,
2951
3031
  region=context.region,
2952
3032
  )
@@ -2998,7 +3078,7 @@ def to_documentation_part_response_json(api_id, data):
2998
3078
 
2999
3079
 
3000
3080
  def to_base_mapping_response_json(domain_name, base_path, data):
3001
- self_link = "/domainnames/%s/basepathmappings/%s" % (domain_name, base_path)
3081
+ self_link = f"/domainnames/{domain_name}/basepathmappings/{base_path}"
3002
3082
  result = to_response_json("basepathmapping", data, self_link=self_link)
3003
3083
  result = select_from_typed_dict(BasePathMapping, result)
3004
3084
  return result
@@ -3034,9 +3114,9 @@ def to_response_json(model_type, data, api_id=None, self_link=None, id_attr=None
3034
3114
  id_attr = id_attr or "id"
3035
3115
  result = deepcopy(data)
3036
3116
  if not self_link:
3037
- self_link = "/%ss/%s" % (model_type, data[id_attr])
3117
+ self_link = f"/{model_type}s/{data[id_attr]}"
3038
3118
  if api_id:
3039
- self_link = "/restapis/%s/%s" % (api_id, self_link)
3119
+ self_link = f"/restapis/{api_id}/{self_link}"
3040
3120
  # TODO: check if this is still required - "_links" are listed in the sample responses in the docs, but
3041
3121
  # recent parity tests indicate that this field is not returned by real AWS...
3042
3122
  # https://docs.aws.amazon.com/apigateway/latest/api/API_GetAuthorizers.html#API_GetAuthorizers_Example_1_Response
@@ -3048,7 +3128,7 @@ def to_response_json(model_type, data, api_id=None, self_link=None, id_attr=None
3048
3128
  "name": model_type,
3049
3129
  "templated": True,
3050
3130
  }
3051
- result["_links"]["%s:delete" % model_type] = {"href": self_link}
3131
+ result["_links"][f"{model_type}:delete"] = {"href": self_link}
3052
3132
  return result
3053
3133
 
3054
3134
 
@@ -68,7 +68,7 @@ class AttributeDict(dict):
68
68
  """
69
69
 
70
70
  def __init__(self, *args, **kwargs):
71
- super(AttributeDict, self).__init__(*args, **kwargs)
71
+ super().__init__(*args, **kwargs)
72
72
  for key, value in self.items():
73
73
  if isinstance(value, dict):
74
74
  self[key] = AttributeDict(value)
@@ -50,7 +50,11 @@ class MethodRequestHandler(RestApiGatewayHandler):
50
50
  # check if there is a validator for this request
51
51
  if not (validator := rest_api.validators.get(request_validator_id)):
52
52
  # TODO Should we raise an exception instead?
53
- LOG.exception("No validator were found with matching id: '%s'", request_validator_id)
53
+ LOG.error(
54
+ "No validator were found with matching id: '%s'",
55
+ request_validator_id,
56
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
57
+ )
54
58
  return
55
59
 
56
60
  if self.should_validate_request(validator) and (
@@ -87,9 +91,10 @@ class MethodRequestHandler(RestApiGatewayHandler):
87
91
  # try to get the resolved model first
88
92
  resolved_schema = model_resolver.get_resolved_model()
89
93
  if not resolved_schema:
90
- LOG.exception(
94
+ LOG.error(
91
95
  "An exception occurred while trying to validate the request: could not resolve the model '%s'",
92
96
  model_name,
97
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
93
98
  )
94
99
  return False
95
100
 
@@ -85,12 +85,11 @@ class RestAPIResourceRouter:
85
85
  rule, args = matcher.match(path, method=request.method, return_rule=True)
86
86
  except (MethodNotAllowed, NotFound) as e:
87
87
  # MethodNotAllowed (405) exception is raised if a path is matching, but the method does not.
88
- # Our router might handle this as a 404, validate with AWS.
88
+ # AWS handles this and the regular 404 as a '403 MissingAuthTokenError'
89
89
  LOG.warning(
90
90
  "API Gateway: No resource or method was found for: %s %s",
91
91
  request.method,
92
92
  path,
93
- exc_info=LOG.isEnabledFor(logging.DEBUG),
94
93
  )
95
94
  raise MissingAuthTokenError("Missing Authentication Token") from e
96
95
 
@@ -211,6 +211,9 @@ class RestApiAwsIntegration(RestApiIntegration):
211
211
  action = parsed_uri["path"]
212
212
 
213
213
  if target := self.get_action_service_target(service_name, action):
214
+ # TODO: properly implement the auto-`Content-Type` headers depending on the service protocol
215
+ # e.g. `x-amz-json-1.0` for DynamoDB
216
+ # this is needed to properly support multi-protocol
214
217
  headers["X-Amz-Target"] = target
215
218
 
216
219
  query_params["Action"] = action
@@ -8,7 +8,7 @@ from werkzeug.datastructures import Headers
8
8
  from localstack.aws.api.apigateway import Integration
9
9
 
10
10
  from ..context import EndpointResponse, IntegrationRequest, RestApiInvocationContext
11
- from ..gateway_response import ApiConfigurationError, IntegrationFailureError
11
+ from ..gateway_response import ApiConfigurationError, IntegrationFailureError, InternalServerError
12
12
  from ..header_utils import build_multi_value_headers
13
13
  from .core import RestApiIntegration
14
14
 
@@ -72,7 +72,7 @@ class RestApiHttpIntegration(BaseRestApiHttpIntegration):
72
72
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
73
73
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
74
74
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
75
- raise ApiConfigurationError("Internal server error") from e
75
+ raise InternalServerError("Internal server error") from e
76
76
 
77
77
  except (requests.exceptions.Timeout, requests.exceptions.SSLError) as e:
78
78
  # TODO make the exception catching more fine grained
@@ -127,7 +127,7 @@ class RestApiHttpProxyIntegration(BaseRestApiHttpIntegration):
127
127
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
128
128
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
129
129
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
130
- raise ApiConfigurationError("Internal server error") from e
130
+ raise InternalServerError("Internal server error") from e
131
131
 
132
132
  except (requests.exceptions.Timeout, requests.exceptions.SSLError):
133
133
  # TODO make the exception catching more fine grained
@@ -109,7 +109,7 @@ class VTLJsonList(list):
109
109
  """
110
110
 
111
111
  def __init__(self, *args):
112
- super(VTLJsonList, self).__init__(*args)
112
+ super().__init__(*args)
113
113
  for idx, item in enumerate(self):
114
114
  self[idx] = cast_to_vtl_json_object(item)
115
115
 
@@ -138,7 +138,7 @@ class AttributeDict(dict):
138
138
  """
139
139
 
140
140
  def __init__(self, *args, **kwargs):
141
- super(AttributeDict, self).__init__(*args, **kwargs)
141
+ super().__init__(*args, **kwargs)
142
142
  for key, value in self.items():
143
143
  if isinstance(value, dict):
144
144
  self[key] = AttributeDict(value)