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
@@ -43,8 +43,8 @@ def generate_ssl_cert(
43
43
  return all(os.path.exists(f) for f in files)
44
44
 
45
45
  def store_cert_key_files(base_filename):
46
- key_file_name = "%s.key" % base_filename
47
- cert_file_name = "%s.crt" % base_filename
46
+ key_file_name = f"{base_filename}.key"
47
+ cert_file_name = f"{base_filename}.crt"
48
48
  # TODO: Cleaner code to load the cert dynamically
49
49
  # extract key and cert from target_file and store into separate files
50
50
  content = load_file(target_file)
@@ -74,9 +74,9 @@ def generate_ssl_cert(
74
74
  return target_file, cert_file_name, key_file_name
75
75
  if random and target_file:
76
76
  if "." in target_file:
77
- target_file = target_file.replace(".", ".%s." % short_uid(), 1)
77
+ target_file = target_file.replace(".", f".{short_uid()}.", 1)
78
78
  else:
79
- target_file = "%s.%s" % (target_file, short_uid())
79
+ target_file = f"{target_file}.{short_uid()}"
80
80
 
81
81
  # create a key pair
82
82
  k = crypto.PKey()
@@ -123,10 +123,10 @@ def generate_ssl_cert(
123
123
  key_file.write(to_str(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)))
124
124
  cert_file_content = cert_file.getvalue().strip()
125
125
  key_file_content = key_file.getvalue().strip()
126
- file_content = "%s\n%s" % (key_file_content, cert_file_content)
126
+ file_content = f"{key_file_content}\n{cert_file_content}"
127
127
  if target_file:
128
- key_file_name = "%s.key" % target_file
129
- cert_file_name = "%s.crt" % target_file
128
+ key_file_name = f"{target_file}.key"
129
+ cert_file_name = f"{target_file}.crt"
130
130
  # check existence to avoid permission denied issues:
131
131
  # https://github.com/localstack/localstack/issues/1607
132
132
  if not all_exist(target_file, key_file_name, cert_file_name):
@@ -145,9 +145,9 @@ def generate_ssl_cert(
145
145
  e,
146
146
  )
147
147
  # Fix for https://github.com/localstack/localstack/issues/1743
148
- target_file = "%s.pem" % new_tmp_file()
149
- key_file_name = "%s.key" % target_file
150
- cert_file_name = "%s.crt" % target_file
148
+ target_file = f"{new_tmp_file()}.pem"
149
+ key_file_name = f"{target_file}.key"
150
+ cert_file_name = f"{target_file}.crt"
151
151
  TMP_FILES.append(target_file)
152
152
  TMP_FILES.append(key_file_name)
153
153
  TMP_FILES.append(cert_file_name)
@@ -3,7 +3,6 @@
3
3
  import inspect
4
4
  import os
5
5
  import socket
6
- from typing import Optional, Union
7
6
 
8
7
  from localstack import config
9
8
  from localstack.constants import DEFAULT_VOLUME_DIR
@@ -77,14 +76,14 @@ def get_localstack_config() -> dict:
77
76
  return result
78
77
 
79
78
 
80
- def inspect_main_container() -> Union[str, dict]:
79
+ def inspect_main_container() -> str | dict:
81
80
  try:
82
81
  return DOCKER_CLIENT.inspect_container(get_main_container_name())
83
82
  except Exception as e:
84
83
  return f"inspect failed: {e}"
85
84
 
86
85
 
87
- def get_localstack_version() -> dict[str, Optional[str]]:
86
+ def get_localstack_version() -> dict[str, str | None]:
88
87
  return {
89
88
  "build-date": os.environ.get("LOCALSTACK_BUILD_DATE"),
90
89
  "build-git-hash": os.environ.get("LOCALSTACK_BUILD_GIT_HASH"),
@@ -134,7 +133,7 @@ def traverse_file_tree(root: str) -> list[str]:
134
133
  result.append(dirpath)
135
134
  return result
136
135
  except Exception as e:
137
- return ["traversing files failed %s" % e]
136
+ return [f"traversing files failed {e}"]
138
137
 
139
138
 
140
139
  def get_docker_image_details() -> dict[str, str]:
@@ -2,13 +2,13 @@ import functools
2
2
  import logging
3
3
  import platform
4
4
  import random
5
- from typing import Optional, Union
6
5
 
7
6
  from localstack import config
8
7
  from localstack.constants import DEFAULT_VOLUME_DIR, DOCKER_IMAGE_NAME
9
8
  from localstack.utils.collections import ensure_list
10
9
  from localstack.utils.container_utils.container_client import (
11
10
  ContainerClient,
11
+ DockerNotAvailable,
12
12
  PortMappings,
13
13
  VolumeInfo,
14
14
  )
@@ -79,7 +79,7 @@ def inspect_current_container_mounts() -> list[VolumeInfo]:
79
79
 
80
80
 
81
81
  @functools.lru_cache
82
- def get_default_volume_dir_mount() -> Optional[VolumeInfo]:
82
+ def get_default_volume_dir_mount() -> VolumeInfo | None:
83
83
  """
84
84
  Returns the volume information of LocalStack's DEFAULT_VOLUME_DIR (/var/lib/localstack), if mounted,
85
85
  else it returns None. If we're not currently in docker a VauleError is raised. in a container, a ValueError is
@@ -132,8 +132,8 @@ def get_host_path_for_path_in_docker(path):
132
132
 
133
133
 
134
134
  def container_ports_can_be_bound(
135
- ports: Union[IntOrPort, list[IntOrPort]],
136
- address: Optional[str] = None,
135
+ ports: IntOrPort | list[IntOrPort],
136
+ address: str | None = None,
137
137
  ) -> bool:
138
138
  """Determine whether a given list of ports can be bound by Docker containers
139
139
 
@@ -153,10 +153,14 @@ def container_ports_can_be_bound(
153
153
  ports=port_mappings,
154
154
  remove=True,
155
155
  )
156
+ except DockerNotAvailable as e:
157
+ LOG.warning("Cannot perform port check because Docker is not available.")
158
+ raise e
156
159
  except Exception as e:
157
160
  if "port is already allocated" not in str(e) and "address already in use" not in str(e):
158
161
  LOG.warning(
159
- "Unexpected error when attempting to determine container port status", exc_info=e
162
+ "Unexpected error when attempting to determine container port status",
163
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
160
164
  )
161
165
  return False
162
166
  # TODO(srw): sometimes the command output from the docker container is "None", particularly when this function is
localstack/utils/files.py CHANGED
@@ -80,9 +80,26 @@ def save_file(file, content, append=False, permissions=None):
80
80
  f.flush()
81
81
 
82
82
 
83
- def load_file(file_path: str, default=None, mode=None):
83
+ def load_file(
84
+ file_path: str | os.PathLike,
85
+ default: str | bytes | None = None,
86
+ mode: str | None = None,
87
+ strict: bool = False,
88
+ ) -> str | bytes | None:
89
+ """
90
+ Return file contents
91
+
92
+ :param file_path: path of the file
93
+ :param default: if strict=False then return this value if the file does not exist
94
+ :param mode: mode to open the file with (e.g. `r`, `rw`)
95
+ :param strict: raise an error if the file path is not a file
96
+ :return: the file contents
97
+ """
84
98
  if not os.path.isfile(file_path):
85
- return default
99
+ if strict:
100
+ raise FileNotFoundError(file_path)
101
+ else:
102
+ return default
86
103
  if not mode:
87
104
  mode = "r"
88
105
  with open(file_path, mode) as f:
@@ -201,7 +218,7 @@ def rm_rf(path: str):
201
218
  # Running the native command can be an order of magnitude faster in Alpine on Travis-CI
202
219
  if is_debian():
203
220
  try:
204
- return run('rm -rf "%s"' % path)
221
+ return run(f'rm -rf "{path}"')
205
222
  except Exception:
206
223
  pass
207
224
  # Make sure all files are writeable and dirs executable to remove
@@ -247,11 +264,7 @@ def cp_r(src: str, dst: str, rm_dest_on_conflict=False, ignore_copystat_errors=F
247
264
  except Exception as e:
248
265
 
249
266
  def _info(_path):
250
- return "%s (file=%s, symlink=%s)" % (
251
- _path,
252
- os.path.isfile(_path),
253
- os.path.islink(_path),
254
- )
267
+ return f"{_path} (file={os.path.isfile(_path)}, symlink={os.path.islink(_path)})"
255
268
 
256
269
  LOG.debug("Error copying files from %s to %s: %s", _info(src), _info(dst), e)
257
270
  raise
@@ -292,7 +305,7 @@ def cleanup_tmp_files():
292
305
  del TMP_FILES[:]
293
306
 
294
307
 
295
- def new_tmp_file(suffix: str = None, dir: str = None) -> str:
308
+ def new_tmp_file(suffix: str | None = None, dir: str | None = None) -> str:
296
309
  """Return a path to a new temporary file."""
297
310
  tmp_file, tmp_path = tempfile.mkstemp(suffix=suffix, dir=dir)
298
311
  os.close(tmp_file)
@@ -300,8 +313,15 @@ def new_tmp_file(suffix: str = None, dir: str = None) -> str:
300
313
  return tmp_path
301
314
 
302
315
 
303
- def new_tmp_dir(dir: str = None):
304
- folder = new_tmp_file(dir=dir)
305
- rm_rf(folder)
306
- mkdir(folder)
316
+ def new_tmp_dir(dir: str | None = None, mode: int = 0o777) -> str:
317
+ """
318
+ Create a new temporary directory with the specified permissions. The directory is added to the tracked temporary
319
+ files.
320
+ :param dir: parent directory for the temporary directory to be created. Systems's default otherwise.
321
+ :param mode: file permission for the directory (default: 0o777)
322
+ :return: the absolute path of the created directory
323
+ """
324
+ folder = tempfile.mkdtemp(dir=dir)
325
+ TMP_FILES.append(folder)
326
+ idempotent_chmod(folder, mode=mode)
307
327
  return folder
@@ -3,7 +3,8 @@
3
3
  import functools
4
4
  import inspect
5
5
  import logging
6
- from typing import Any, Callable, Optional
6
+ from collections.abc import Callable
7
+ from typing import Any
7
8
 
8
9
  LOG = logging.getLogger(__name__)
9
10
 
@@ -20,7 +21,7 @@ def run_safe(_python_lambda, *args, _default=None, **kwargs):
20
21
 
21
22
  def call_safe(
22
23
  func: Callable, args: tuple = None, kwargs: dict = None, exception_message: str = None
23
- ) -> Optional[Any]:
24
+ ) -> Any | None:
24
25
  """
25
26
  Call the given function with the given arguments, and if it fails, log the given exception_message.
26
27
  If logging.DEBUG is set for the logger, then we also log the traceback.
@@ -32,7 +33,7 @@ def call_safe(
32
33
  :return: whatever the func returns
33
34
  """
34
35
  if exception_message is None:
35
- exception_message = "error calling function %s" % func.__name__
36
+ exception_message = f"error calling function {func.__name__}"
36
37
  if args is None:
37
38
  args = ()
38
39
  if kwargs is None:
localstack/utils/http.py CHANGED
@@ -2,7 +2,6 @@ import logging
2
2
  import math
3
3
  import os
4
4
  import re
5
- from typing import Optional, Union
6
5
  from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse
7
6
 
8
7
  import requests
@@ -43,18 +42,18 @@ def create_chunked_data(data, chunk_size: int = 80):
43
42
  dl = len(data)
44
43
  ret = ""
45
44
  for i in range(dl // chunk_size):
46
- ret += "%s\r\n" % (hex(chunk_size)[2:])
47
- ret += "%s\r\n\r\n" % (data[i * chunk_size : (i + 1) * chunk_size])
45
+ ret += f"{hex(chunk_size)[2:]}\r\n"
46
+ ret += f"{data[i * chunk_size : (i + 1) * chunk_size]}\r\n\r\n"
48
47
 
49
48
  if len(data) % chunk_size != 0:
50
- ret += "%s\r\n" % (hex(len(data) % chunk_size)[2:])
51
- ret += "%s\r\n" % (data[-(len(data) % chunk_size) :])
49
+ ret += f"{hex(len(data) % chunk_size)[2:]}\r\n"
50
+ ret += f"{data[-(len(data) % chunk_size) :]}\r\n"
52
51
 
53
52
  ret += "0\r\n\r\n"
54
53
  return ret
55
54
 
56
55
 
57
- def canonicalize_headers(headers: Union[dict, CaseInsensitiveDict]) -> dict:
56
+ def canonicalize_headers(headers: dict | CaseInsensitiveDict) -> dict:
58
57
  if not headers:
59
58
  return headers
60
59
 
@@ -103,7 +102,7 @@ def add_query_params_to_url(uri: str, query_params: dict) -> str:
103
102
 
104
103
 
105
104
  def make_http_request(
106
- url: str, data: Union[bytes, str] = None, headers: dict[str, str] = None, method: str = "GET"
105
+ url: str, data: bytes | str = None, headers: dict[str, str] = None, method: str = "GET"
107
106
  ) -> Response:
108
107
  return requests.request(
109
108
  url=url, method=method, headers=headers, data=data, auth=NetrcBypassAuth(), verify=False
@@ -179,7 +178,7 @@ def download(
179
178
  path: str,
180
179
  verify_ssl: bool = True,
181
180
  timeout: float = None,
182
- request_headers: Optional[dict] = None,
181
+ request_headers: dict | None = None,
183
182
  quiet: bool = False,
184
183
  ) -> None:
185
184
  """Downloads file at url to the given path. Raises TimeoutError if the optional timeout (in secs) is reached.
@@ -202,7 +201,7 @@ def download(
202
201
  r = s.get(url, stream=True, verify=_verify, timeout=timeout, headers=request_headers)
203
202
  # check status code before attempting to read body
204
203
  if not r.ok:
205
- raise Exception("Failed to download %s, response code %s" % (url, r.status_code))
204
+ raise Exception(f"Failed to download {url}, response code {r.status_code}")
206
205
 
207
206
  total_size = 0
208
207
  if r.headers.get("Content-Length"):
@@ -291,18 +290,19 @@ def download_github_artifact(url: str, target_file: str, timeout: int = None):
291
290
  Optionally allows to define a timeout in seconds."""
292
291
 
293
292
  def do_download(
294
- download_url: str, request_headers: Optional[dict] = None, print_error: bool = False
293
+ download_url: str, request_headers: dict | None = None, print_error: bool = False
295
294
  ):
296
295
  try:
297
296
  download(download_url, target_file, timeout=timeout, request_headers=request_headers)
298
297
  return True
299
298
  except Exception as e:
300
299
  if print_error:
301
- LOG.exception(
300
+ LOG.error(
302
301
  "Unable to download Github artifact from %s to %s: %s %s",
303
302
  url,
304
303
  target_file,
305
304
  e,
305
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
306
306
  )
307
307
 
308
308
  # if a GitHub API token is set, use it to avoid rate limiting issues
localstack/utils/json.py CHANGED
@@ -5,7 +5,7 @@ import logging
5
5
  import os
6
6
  from datetime import date, datetime
7
7
  from json import JSONDecodeError
8
- from typing import Any, Union
8
+ from typing import Any
9
9
 
10
10
  from localstack.config import HostAndPort
11
11
 
@@ -42,7 +42,7 @@ class CustomEncoder(json.JSONEncoder):
42
42
  try:
43
43
  if isinstance(o, bytes):
44
44
  return to_str(o)
45
- return super(CustomEncoder, self).default(o)
45
+ return super().default(o)
46
46
  except Exception:
47
47
  return None
48
48
 
@@ -63,9 +63,9 @@ class FileMappedDocument(dict):
63
63
  concurrent writes, run load(). To save and overwrite the current document on disk, run save().
64
64
  """
65
65
 
66
- path: Union[str, os.PathLike]
66
+ path: str | os.PathLike
67
67
 
68
- def __init__(self, path: Union[str, os.PathLike], mode=0o664):
68
+ def __init__(self, path: str | os.PathLike, mode=0o664):
69
69
  super().__init__()
70
70
  self.path = path
71
71
  self.mode = mode
@@ -169,10 +169,24 @@ def extract_jsonpath(value, path):
169
169
  return result
170
170
 
171
171
 
172
- def assign_to_path(target, path: str, value, delimiter: str = "."):
172
+ def assign_to_path(target: dict, path: str, value: any, delimiter: str = ".") -> dict:
173
+ """Assign the given value to a dict. If the path doesn't exist in the target dict, it will be created.
174
+ The delimiter can be used to provide a path with a different delimiter.
175
+
176
+ Examples:
177
+ - assign_to_path({}, "a", "b") => {"a": "b"}
178
+ - assign_to_path({}, "a.b.c", "d") => {"a": {"b": {"c": "d"}}}
179
+ - assign_to_path({}, "a.b/c", "d", delimiter="/") => {"a.b": {"c": "d"}}
180
+
181
+ """
173
182
  parts = path.strip(delimiter).split(delimiter)
183
+
184
+ if len(parts) == 1:
185
+ target[parts[0]] = value
186
+ return target
187
+
174
188
  path_to_parent = delimiter.join(parts[:-1])
175
- parent = extract_from_jsonpointer_path(target, path_to_parent, auto_create=True)
189
+ parent = extract_from_jsonpointer_path(target, path_to_parent, delimiter, auto_create=True)
176
190
  if not isinstance(parent, dict):
177
191
  LOG.debug(
178
192
  'Unable to find parent (type %s) for path "%s" in object: %s',
@@ -6,7 +6,8 @@ import re
6
6
  import socket
7
7
  import tempfile
8
8
  import threading
9
- from typing import Any, Callable
9
+ from collections.abc import Callable
10
+ from typing import Any
10
11
 
11
12
  from amazon_kclpy import kcl
12
13
  from amazon_kclpy.v2 import processor
localstack/utils/net.py CHANGED
@@ -5,7 +5,7 @@ import socket
5
5
  import threading
6
6
  from collections.abc import MutableMapping
7
7
  from contextlib import closing
8
- from typing import Any, NamedTuple, Optional, Union
8
+ from typing import Any, NamedTuple
9
9
  from urllib.parse import urlparse
10
10
 
11
11
  import dns.resolver
@@ -50,14 +50,14 @@ class Port(NamedTuple):
50
50
 
51
51
 
52
52
  # simple helper type to encapsulate int/Port argument types
53
- IntOrPort = Union[int, Port]
53
+ IntOrPort = int | Port
54
54
 
55
55
 
56
56
  def is_port_open(
57
- port_or_url: Union[int, str],
57
+ port_or_url: int | str,
58
58
  http_path: str = None,
59
59
  expect_success: bool = True,
60
- protocols: Optional[Union[str, list[str]]] = None,
60
+ protocols: str | list[str] | None = None,
61
61
  quiet: bool = True,
62
62
  ):
63
63
  from localstack.utils.http import safe_requests
@@ -97,7 +97,12 @@ def is_port_open(
97
97
  sock.recvfrom(1024)
98
98
  except Exception:
99
99
  if not quiet:
100
- LOG.exception("Error connecting to UDP port %s:%s", host, port)
100
+ LOG.error(
101
+ "Error connecting to UDP port %s:%s",
102
+ host,
103
+ port,
104
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
105
+ )
101
106
  return False
102
107
  elif nw_protocol == socket.SOCK_STREAM:
103
108
  result = sock.connect_ex((host, port))
@@ -159,8 +164,9 @@ def wait_for_port_status(
159
164
  status = is_port_open(port, http_path=http_path, expect_success=expect_success)
160
165
  if bool(status) != (not expect_closed):
161
166
  raise Exception(
162
- "Port %s (path: %s) was not %s"
163
- % (port, http_path, "closed" if expect_closed else "open")
167
+ "Port {} (path: {}) was not {}".format(
168
+ port, http_path, "closed" if expect_closed else "open"
169
+ )
164
170
  )
165
171
 
166
172
  return retry(check, sleep=sleep_time, retries=retries)
@@ -269,7 +275,7 @@ def get_free_tcp_port_range(num_ports: int, max_attempts: int = 50) -> "PortRang
269
275
  raise PortNotAvailableException("reached max_attempts when trying to find port range")
270
276
 
271
277
 
272
- def resolve_hostname(hostname: str) -> Optional[str]:
278
+ def resolve_hostname(hostname: str) -> str | None:
273
279
  """Resolve the given hostname and return its IP address, or None if it cannot be resolved."""
274
280
  try:
275
281
  return socket.gethostbyname(hostname)
@@ -356,7 +362,7 @@ class PortRange:
356
362
  """
357
363
  return range(self.start, self.end + 1)
358
364
 
359
- def reserve_port(self, port: Optional[IntOrPort] = None, duration: Optional[int] = None) -> int:
365
+ def reserve_port(self, port: IntOrPort | None = None, duration: int | None = None) -> int:
360
366
  """
361
367
  Reserves the given port (if it is still free). If the given port is None, it reserves a free port from the
362
368
  configured port range for external services. If a port is given, it has to be within the configured
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
  import logging
3
- from typing import NoReturn, Optional
3
+ from typing import NoReturn
4
4
 
5
5
  LOG = logging.getLogger(__name__)
6
6
 
@@ -12,7 +12,7 @@ class NoExitArgumentParser(argparse.ArgumentParser):
12
12
  * ArgumentParser subclassing example: https://stackoverflow.com/a/59072378/6875981
13
13
  """
14
14
 
15
- def exit(self, status: int = ..., message: Optional[str] = ...) -> NoReturn:
15
+ def exit(self, status: int = ..., message: str | None = ...) -> NoReturn:
16
16
  LOG.warning("Error in argument parser but preventing exit: %s", message)
17
17
 
18
18
  def error(self, message: str) -> NoReturn:
@@ -1,4 +1,4 @@
1
- from typing import Any, Union
1
+ from typing import Any
2
2
 
3
3
 
4
4
  def format_number(number: float, decimals: int = 2):
@@ -11,6 +11,13 @@ def format_number(number: float, decimals: int = 2):
11
11
 
12
12
 
13
13
  def is_number(s: Any) -> bool:
14
+ # booleans inherit from int
15
+ #
16
+ # >>> a.__class__.__mro__
17
+ # (<class 'bool'>, <class 'int'>, <class 'object'>)
18
+ if s is False or s is True:
19
+ return False
20
+
14
21
  try:
15
22
  float(s) # for int, long and float
16
23
  return True
@@ -18,7 +25,7 @@ def is_number(s: Any) -> bool:
18
25
  return False
19
26
 
20
27
 
21
- def to_number(s: Any) -> Union[int, float]:
28
+ def to_number(s: Any) -> int | float:
22
29
  """Cast the string representation of the given object to a number (int or float), or raise ValueError."""
23
30
  try:
24
31
  return int(str(s))
@@ -1,12 +1,13 @@
1
1
  import functools
2
2
  import re
3
3
  import threading
4
- from typing import Any, Callable, Generic, Optional, TypeVar, Union
4
+ from collections.abc import Callable
5
+ from typing import Any, Generic, TypeVar
5
6
 
6
7
  from .collections import ensure_list
7
8
  from .strings import first_char_to_lower, first_char_to_upper
8
9
 
9
- ComplexType = Union[list, dict, object]
10
+ ComplexType = list | dict | object
10
11
 
11
12
  _T = TypeVar("_T")
12
13
 
@@ -16,7 +17,7 @@ class Value(Generic[_T]):
16
17
  Simple value container.
17
18
  """
18
19
 
19
- value: Optional[_T]
20
+ value: _T | None
20
21
 
21
22
  def __init__(self, value: _T = None) -> None:
22
23
  self.value = value
@@ -30,7 +31,7 @@ class Value(Generic[_T]):
30
31
  def is_set(self) -> bool:
31
32
  return self.value is not None
32
33
 
33
- def get(self) -> Optional[_T]:
34
+ def get(self) -> _T | None:
34
35
  return self.value
35
36
 
36
37
  def __bool__(self):
@@ -136,7 +137,7 @@ def fully_qualified_class_name(klass: type) -> str:
136
137
  return f"{klass.__module__}.{klass.__name__}"
137
138
 
138
139
 
139
- def not_none_or(value: Optional[Any], alternative: Any) -> Any:
140
+ def not_none_or(value: Any | None, alternative: Any) -> Any:
140
141
  """Return 'value' if it is not None, or 'alternative' otherwise."""
141
142
  return value if value is not None else alternative
142
143
 
@@ -163,7 +164,7 @@ def keys_to(
163
164
  skip_children_of = ensure_list(skip_children_of or [])
164
165
 
165
166
  def fix_keys(o, path="", **kwargs):
166
- if any(re.match(r"(^|.*\.)%s($|[.\[].*)" % k, path) for k in skip_children_of):
167
+ if any(re.match(rf"(^|.*\.){k}($|[.\[].*)", path) for k in skip_children_of):
167
168
  return o
168
169
  if isinstance(o, dict):
169
170
  for k, v in dict(o).items():
localstack/utils/patch.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import functools
2
2
  import inspect
3
3
  import types
4
- from typing import Any, Callable
4
+ from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
7
 
7
8
  def get_defining_object(method):
@@ -97,19 +98,25 @@ class Patch:
97
98
  self.is_applied = False
98
99
 
99
100
  def apply(self):
101
+ if self.is_applied:
102
+ return
103
+
100
104
  if self.old and self.name == "__getattr__":
101
105
  raise Exception("You can't patch class types implementing __getattr__")
102
106
  if not self.old and self.name != "__getattr__":
103
107
  raise AttributeError(f"`{self.obj.__name__}` object has no attribute `{self.name}`")
104
108
  setattr(self.obj, self.name, self.new)
105
- self.is_applied = True
106
109
  Patch.applied_patches.append(self)
110
+ self.is_applied = True
107
111
 
108
112
  def undo(self):
113
+ if not self.is_applied:
114
+ return
115
+
109
116
  # If we added a method to a class type, we don't have a self.old. We just delete __getattr__
110
117
  setattr(self.obj, self.name, self.old) if self.old else delattr(self.obj, self.name)
111
- self.is_applied = False
112
118
  Patch.applied_patches.remove(self)
119
+ self.is_applied = False
113
120
 
114
121
  def __enter__(self):
115
122
  self.apply()