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
localstack/utils/run.py CHANGED
@@ -7,9 +7,10 @@ import subprocess
7
7
  import sys
8
8
  import threading
9
9
  import time
10
+ from collections.abc import Callable
10
11
  from functools import lru_cache
11
12
  from queue import Queue
12
- from typing import Any, AnyStr, Callable, Optional, Union
13
+ from typing import Any, AnyStr
13
14
 
14
15
  from localstack import config
15
16
 
@@ -23,19 +24,19 @@ LOG = logging.getLogger(__name__)
23
24
 
24
25
 
25
26
  def run(
26
- cmd: Union[str, list[str]],
27
+ cmd: str | list[str],
27
28
  print_error=True,
28
29
  asynchronous=False,
29
30
  stdin=False,
30
31
  stderr=subprocess.STDOUT,
31
32
  outfile=None,
32
- env_vars: Optional[dict[AnyStr, AnyStr]] = None,
33
+ env_vars: dict[AnyStr, AnyStr] | None = None,
33
34
  inherit_cwd=False,
34
35
  inherit_env=True,
35
36
  tty=False,
36
37
  shell=True,
37
38
  cwd: str = None,
38
- ) -> Union[str, subprocess.Popen]:
39
+ ) -> str | subprocess.Popen:
39
40
  LOG.debug("Executing command: %s", cmd)
40
41
  env_dict = os.environ.copy() if inherit_env else {}
41
42
  if env_vars:
@@ -114,7 +115,7 @@ def run(
114
115
  return process
115
116
  except subprocess.CalledProcessError as e:
116
117
  if print_error:
117
- print("ERROR: '%s': exit code %s; output: %s" % (cmd, e.returncode, e.output))
118
+ print(f"ERROR: '{cmd}': exit code {e.returncode}; output: {e.output}")
118
119
  sys.stdout.flush()
119
120
  raise e
120
121
 
@@ -202,7 +203,7 @@ def get_os_user() -> str:
202
203
  return run("whoami").strip()
203
204
 
204
205
 
205
- def to_str(obj: Union[str, bytes], errors="strict"):
206
+ def to_str(obj: str | bytes, errors="strict"):
206
207
  return obj.decode(config.DEFAULT_ENCODING, errors) if isinstance(obj, bytes) else obj
207
208
 
208
209
 
@@ -211,9 +212,9 @@ class ShellCommandThread(FuncThread):
211
212
 
212
213
  def __init__(
213
214
  self,
214
- cmd: Union[str, list[str]],
215
+ cmd: str | list[str],
215
216
  params: Any = None,
216
- outfile: Union[str, int] = None,
217
+ outfile: str | int = None,
217
218
  env_vars: dict[str, str] = None,
218
219
  stdin: bool = False,
219
220
  auto_restart: bool = False,
@@ -223,8 +224,8 @@ class ShellCommandThread(FuncThread):
223
224
  log_listener: Callable = None,
224
225
  stop_listener: Callable = None,
225
226
  strip_color: bool = False,
226
- name: Optional[str] = None,
227
- cwd: Optional[str] = None,
227
+ name: str | None = None,
228
+ cwd: str | None = None,
228
229
  ):
229
230
  params = params if params is not None else {}
230
231
  env_vars = env_vars if env_vars is not None else {}
@@ -268,7 +269,7 @@ class ShellCommandThread(FuncThread):
268
269
  if self.strip_color:
269
270
  # strip color codes
270
271
  line = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", line)
271
- return "%s\r\n" % line.strip()
272
+ return f"{line.strip()}\r\n"
272
273
 
273
274
  def filter_line(line):
274
275
  """Return True if this line should be filtered, i.e., not printed"""
@@ -1,9 +1,9 @@
1
1
  import queue
2
2
  import threading
3
3
  import time
4
- from collections.abc import Mapping
4
+ from collections.abc import Callable, Mapping
5
5
  from concurrent.futures import Executor
6
- from typing import Any, Callable, Optional, Union
6
+ from typing import Any
7
7
 
8
8
 
9
9
  class ScheduledTask:
@@ -14,12 +14,12 @@ class ScheduledTask:
14
14
  def __init__(
15
15
  self,
16
16
  task: Callable,
17
- period: Optional[float] = None,
17
+ period: float | None = None,
18
18
  fixed_rate: bool = True,
19
- start: Optional[float] = None,
19
+ start: float | None = None,
20
20
  on_error: Callable[[Exception], None] = None,
21
- args: Optional[Union[tuple, list]] = None,
22
- kwargs: Optional[Mapping[str, Any]] = None,
21
+ args: tuple | list | None = None,
22
+ kwargs: Mapping[str, Any] | None = None,
23
23
  ) -> None:
24
24
  super().__init__()
25
25
  self.task = task
@@ -76,7 +76,7 @@ class Scheduler:
76
76
 
77
77
  POISON = (-1, "__POISON__")
78
78
 
79
- def __init__(self, executor: Optional[Executor] = None) -> None:
79
+ def __init__(self, executor: Executor | None = None) -> None:
80
80
  """
81
81
  Creates a new Scheduler. If an executor is passed, then that executor will be used to run the scheduled tasks
82
82
  asynchronously, otherwise they will be executed synchronously inside the event loop. Running tasks
@@ -94,12 +94,12 @@ class Scheduler:
94
94
  def schedule(
95
95
  self,
96
96
  func: Callable,
97
- period: Optional[float] = None,
97
+ period: float | None = None,
98
98
  fixed_rate: bool = True,
99
- start: Optional[float] = None,
99
+ start: float | None = None,
100
100
  on_error: Callable[[Exception], None] = None,
101
- args: Optional[Union[tuple, list[Any]]] = None,
102
- kwargs: Optional[Mapping[str, Any]] = None,
101
+ args: tuple | list[Any] | None = None,
102
+ kwargs: Mapping[str, Any] | None = None,
103
103
  ) -> ScheduledTask:
104
104
  """
105
105
  Schedules a given task (function call).
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  import select
3
3
  import socket
4
+ from collections.abc import Callable
4
5
  from concurrent.futures import ThreadPoolExecutor
5
- from typing import Callable
6
6
 
7
7
  from localstack.utils.serving import Server
8
8
 
@@ -93,7 +93,7 @@ class TCPProxy(Server):
93
93
  try:
94
94
  src_socket, _ = self._server_socket.accept()
95
95
  self._thread_pool.submit(self._handle_request, src_socket)
96
- except socket.timeout:
96
+ except TimeoutError:
97
97
  pass
98
98
  except OSError as e:
99
99
  # avoid creating an error message if OSError is thrown due to socket closing
@@ -1,7 +1,6 @@
1
1
  import abc
2
2
  import logging
3
3
  import threading
4
- from typing import Optional
5
4
 
6
5
  from localstack.utils.net import is_port_open
7
6
  from localstack.utils.sync import poll_condition
@@ -21,7 +20,7 @@ class Server(abc.ABC):
21
20
 
22
21
  def __init__(self, port: int, host: str = "localhost") -> None:
23
22
  super().__init__()
24
- self._thread: Optional[FuncThread] = None
23
+ self._thread: FuncThread | None = None
25
24
 
26
25
  self._lifecycle_lock = threading.RLock()
27
26
  self._stopped = threading.Event()
@@ -44,9 +43,9 @@ class Server(abc.ABC):
44
43
 
45
44
  @property
46
45
  def url(self):
47
- return "%s://%s:%s" % (self.protocol, self.host, self.port)
46
+ return f"{self.protocol}://{self.host}:{self.port}"
48
47
 
49
- def get_error(self) -> Optional[Exception]:
48
+ def get_error(self) -> Exception | None:
50
49
  """
51
50
  If the thread running the server returned with an Exception, then this function will return that exception.
52
51
  """
@@ -7,7 +7,6 @@ import re
7
7
  import string
8
8
  import uuid
9
9
  import zlib
10
- from typing import Union
11
10
 
12
11
  from localstack.config import DEFAULT_ENCODING
13
12
 
@@ -28,13 +27,13 @@ REGEX_UNPRINTABLE_CHARS = re.compile(
28
27
  )
29
28
 
30
29
 
31
- def to_str(obj: Union[str, bytes], encoding: str = DEFAULT_ENCODING, errors="strict") -> str:
30
+ def to_str(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> str:
32
31
  """If ``obj`` is an instance of ``binary_type``, return
33
32
  ``obj.decode(encoding, errors)``, otherwise return ``obj``"""
34
33
  return obj.decode(encoding, errors) if isinstance(obj, bytes) else obj
35
34
 
36
35
 
37
- def to_bytes(obj: Union[str, bytes], encoding: str = DEFAULT_ENCODING, errors="strict") -> bytes:
36
+ def to_bytes(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> bytes:
38
37
  """If ``obj`` is an instance of ``text_type``, return
39
38
  ``obj.encode(encoding, errors)``, otherwise return ``obj``"""
40
39
  return obj.encode(encoding, errors) if isinstance(obj, str) else obj
@@ -42,7 +41,7 @@ def to_bytes(obj: Union[str, bytes], encoding: str = DEFAULT_ENCODING, errors="s
42
41
 
43
42
  def truncate(data: str, max_length: int = 100) -> str:
44
43
  data = str(data or "")
45
- return ("%s..." % data[:max_length]) if len(data) > max_length else data
44
+ return (f"{data[:max_length]}...") if len(data) > max_length else data
46
45
 
47
46
 
48
47
  def is_string(s, include_unicode=True, exclude_binary=False):
@@ -86,7 +85,7 @@ def canonicalize_bool_to_str(val: bool) -> str:
86
85
  return "true" if str(val).lower() == "true" else "false"
87
86
 
88
87
 
89
- def convert_to_printable_chars(value: Union[list, dict, str]) -> str:
88
+ def convert_to_printable_chars(value: list | dict | str) -> str:
90
89
  """Removes all unprintable characters from the given string."""
91
90
  from localstack.utils.objects import recurse_object
92
91
 
@@ -104,11 +103,11 @@ def convert_to_printable_chars(value: Union[list, dict, str]) -> str:
104
103
 
105
104
 
106
105
  def first_char_to_lower(s: str) -> str:
107
- return s and "%s%s" % (s[0].lower(), s[1:])
106
+ return s and f"{s[0].lower()}{s[1:]}"
108
107
 
109
108
 
110
109
  def first_char_to_upper(s: str) -> str:
111
- return s and "%s%s" % (s[0].upper(), s[1:])
110
+ return s and f"{s[0].upper()}{s[1:]}"
112
111
 
113
112
 
114
113
  def str_to_bool(value):
@@ -121,13 +120,13 @@ def str_to_bool(value):
121
120
 
122
121
  def str_insert(string, index, content):
123
122
  """Insert a substring into an existing string at a certain index."""
124
- return "%s%s%s" % (string[:index], content, string[index:])
123
+ return f"{string[:index]}{content}{string[index:]}"
125
124
 
126
125
 
127
126
  def str_remove(string, index, end_index=None):
128
127
  """Remove a substring from an existing string at a certain from-to index range."""
129
128
  end_index = end_index or (index + 1)
130
- return "%s%s" % (string[:index], string[end_index:])
129
+ return f"{string[:index]}{string[end_index:]}"
131
130
 
132
131
 
133
132
  def str_startswith_ignore_case(value: str, prefix: str) -> bool:
@@ -148,19 +147,19 @@ def long_uid() -> str:
148
147
  return str(uuid.uuid4())
149
148
 
150
149
 
151
- def md5(string: Union[str, bytes]) -> str:
150
+ def md5(string: str | bytes) -> str:
152
151
  m = hashlib.md5()
153
152
  m.update(to_bytes(string))
154
153
  return m.hexdigest()
155
154
 
156
155
 
157
- def checksum_crc32(string: Union[str, bytes]) -> str:
156
+ def checksum_crc32(string: str | bytes) -> str:
158
157
  bytes = to_bytes(string)
159
158
  checksum = zlib.crc32(bytes)
160
159
  return base64.b64encode(checksum.to_bytes(4, "big")).decode()
161
160
 
162
161
 
163
- def checksum_crc32c(string: Union[str, bytes]):
162
+ def checksum_crc32c(string: str | bytes):
164
163
  # import botocore locally here to avoid a dependency of the CLI to botocore
165
164
  from botocore.httpchecksum import CrtCrc32cChecksum
166
165
 
@@ -169,7 +168,7 @@ def checksum_crc32c(string: Union[str, bytes]):
169
168
  return base64.b64encode(checksum.digest()).decode()
170
169
 
171
170
 
172
- def checksum_crc64nvme(string: Union[str, bytes]):
171
+ def checksum_crc64nvme(string: str | bytes):
173
172
  # import botocore locally here to avoid a dependency of the CLI to botocore
174
173
  from botocore.httpchecksum import CrtCrc64NvmeChecksum
175
174
 
@@ -178,12 +177,12 @@ def checksum_crc64nvme(string: Union[str, bytes]):
178
177
  return base64.b64encode(checksum.digest()).decode()
179
178
 
180
179
 
181
- def hash_sha1(string: Union[str, bytes]) -> str:
180
+ def hash_sha1(string: str | bytes) -> str:
182
181
  digest = hashlib.sha1(to_bytes(string)).digest()
183
182
  return base64.b64encode(digest).decode()
184
183
 
185
184
 
186
- def hash_sha256(string: Union[str, bytes]) -> str:
185
+ def hash_sha256(string: str | bytes) -> str:
187
186
  digest = hashlib.sha256(to_bytes(string)).digest()
188
187
  return base64.b64encode(digest).decode()
189
188
 
@@ -192,7 +191,7 @@ def base64_to_hex(b64_string: str) -> bytes:
192
191
  return binascii.hexlify(base64.b64decode(b64_string))
193
192
 
194
193
 
195
- def base64_decode(data: Union[str, bytes]) -> bytes:
194
+ def base64_decode(data: str | bytes) -> bytes:
196
195
  """Decode base64 data - with optional padding, and able to handle urlsafe encoding (containing -/_)."""
197
196
  data = to_str(data)
198
197
  missing_padding = len(data) % 4
localstack/utils/sync.py CHANGED
@@ -4,7 +4,8 @@ import functools
4
4
  import threading
5
5
  import time
6
6
  from collections import defaultdict
7
- from typing import Callable, Literal, TypeVar
7
+ from collections.abc import Callable
8
+ from typing import Literal, TypeVar
8
9
 
9
10
 
10
11
  class ShortCircuitWaitException(Exception):
@@ -140,3 +141,127 @@ class SynchronizedDefaultDict(defaultdict):
140
141
  def __str__(self):
141
142
  with self._lock:
142
143
  return super().__str__()
144
+
145
+
146
+ class Once:
147
+ """
148
+ An object that will perform an action exactly once.
149
+ Inspired by Golang's [sync.Once](https://pkg.go.dev/sync#Once) operation.
150
+
151
+
152
+ ### Example 1
153
+
154
+ Multiple threads using `Once::do` to ensure only 1 line is printed.
155
+
156
+ ```python
157
+ import threading
158
+ import time
159
+ import random
160
+
161
+ greet_once = Once()
162
+ def greet():
163
+ print("This should happen only once.")
164
+
165
+ greet_threads = []
166
+ for _ in range(10):
167
+ t = threading.Thread(target=lambda: greet_once.do(greet))
168
+ greet_threads.append(t)
169
+ t.start()
170
+
171
+ for t in greet_threads:
172
+ t.join()
173
+ ```
174
+
175
+
176
+ ### Example 2
177
+
178
+ Ensuring idemponent calling to prevent exceptions on multiple calls.
179
+
180
+ ```python
181
+ import os
182
+
183
+ class Service:
184
+ close_once: sync.Once
185
+
186
+ def start(self):
187
+ with open("my-service.txt) as f:
188
+ myfile.write("Started service")
189
+
190
+ def close(self):
191
+ # Ensure we only ever delete the file once on close
192
+ self.close_once.do(lambda: os.remove("my-service.txt"))
193
+
194
+ ```
195
+
196
+
197
+ """
198
+
199
+ _is_done: bool = False
200
+ _mu: threading.Lock = threading.Lock()
201
+
202
+ def do(self, fn: Callable[[], None]):
203
+ """
204
+ `do` calls the function `fn()` if-and-only-if `do` has never been called before.
205
+
206
+ This ensures idempotent and thread-safe execution.
207
+
208
+ If the function raises an exception, `do` considers `fn` as done, where subsequent calls are still no-ops.
209
+ """
210
+ if self._is_done:
211
+ return
212
+
213
+ with self._mu:
214
+ if not self._is_done:
215
+ try:
216
+ fn()
217
+ finally:
218
+ self._is_done = True
219
+
220
+
221
+ def once_func(fn: Callable[..., T]) -> Callable[..., T | None]:
222
+ """
223
+ Wraps and returns a function that can only ever execute once.
224
+
225
+ The first call to the returned function will permanently set the result.
226
+ If the wrapped function raises an exception, this will be re-raised on each subsequent call.
227
+
228
+ This function can be used either as a decorator or called directly.
229
+
230
+ Direct usage:
231
+ ```python
232
+ delete_file = once_func(os.remove)
233
+
234
+ delete_file("myfile.txt") # deletes the file
235
+ delete_file("myfile.txt") # does nothing
236
+ ```
237
+
238
+ As a decorator:
239
+ ```python
240
+ @once_func
241
+ def delete_file():
242
+ os.remove("myfile.txt")
243
+
244
+ delete_file() # deletes the file
245
+ delete_file() # does nothing
246
+ ```
247
+ """
248
+ once = Once()
249
+
250
+ result, exception = None, None
251
+
252
+ def _do(*args, **kwargs):
253
+ nonlocal result, exception
254
+ try:
255
+ result = fn(*args, **kwargs)
256
+ except Exception as e:
257
+ exception = e
258
+ raise
259
+
260
+ @functools.wraps(fn)
261
+ def wrapper(*args, **kwargs):
262
+ once.do(lambda: _do(*args, **kwargs))
263
+ if exception is not None:
264
+ raise exception
265
+ return result
266
+
267
+ return wrapper
@@ -1,18 +1,20 @@
1
- from typing import Optional
1
+ class TaggingService:
2
+ key_field: str
3
+ value_field: str
2
4
 
5
+ tags: dict[str, dict[str, str]]
3
6
 
4
- class TaggingService:
5
- def __init__(self, key_field: str = None, value_field: str = None):
7
+ def __init__(self, key_field: str = "Key", value_field: str = "Value"):
6
8
  """
7
9
  :param key_field: the field name representing the tag key as used by botocore specs
8
10
  :param value_field: the field name representing the tag value as used by botocore specs
9
11
  """
10
- self.key_field = key_field or "Key"
11
- self.value_field = value_field or "Value"
12
+ self.key_field = key_field
13
+ self.value_field = value_field
12
14
 
13
15
  self.tags = {}
14
16
 
15
- def list_tags_for_resource(self, arn: str, root_name: Optional[str] = None):
17
+ def list_tags_for_resource(self, arn: str, root_name: str | None = None):
16
18
  root_name = root_name or "Tags"
17
19
 
18
20
  result = []
@@ -7,7 +7,8 @@ import re
7
7
  import shutil
8
8
  import tempfile
9
9
  import time
10
- from typing import Any, Callable, Optional
10
+ from collections.abc import Callable
11
+ from typing import Any
11
12
 
12
13
  from localstack.aws.api.lambda_ import Runtime
13
14
  from localstack.aws.connect import connect_externally_to, connect_to
@@ -20,7 +21,7 @@ from localstack.utils.urls import localstack_host
20
21
  try:
21
22
  from typing import Literal
22
23
  except ImportError:
23
- from typing_extensions import Literal
24
+ from typing import Literal
24
25
 
25
26
  import boto3
26
27
  import requests
@@ -94,7 +95,7 @@ def create_lambda_archive(
94
95
  chmod_r(script_file, 0o777)
95
96
  # copy libs
96
97
  for lib in libs:
97
- paths = [lib, "%s.py" % lib]
98
+ paths = [lib, f"{lib}.py"]
98
99
  try:
99
100
  module = importlib.import_module(lib)
100
101
  paths.append(module.__file__)
@@ -382,7 +383,7 @@ def assert_object(expected_object, all_objects):
382
383
  all_objects = [all_objects]
383
384
  found = find_object(expected_object, all_objects)
384
385
  if not found:
385
- raise Exception("Expected object not found: %s in list %s" % (expected_object, all_objects))
386
+ raise Exception(f"Expected object not found: {expected_object} in list {all_objects}")
386
387
 
387
388
 
388
389
  def find_object(expected_object, object_list):
@@ -522,8 +523,7 @@ def check_expected_lambda_log_events_length(
522
523
  events = [line for line in events if line not in ["\x1b[0m", "\\x1b[0m"]]
523
524
  if len(events) != expected_length:
524
525
  print(
525
- "Invalid # of Lambda %s log events: %s / %s: %s"
526
- % (
526
+ "Invalid # of Lambda {} log events: {} / {}: {}".format(
527
527
  function_name,
528
528
  len(events),
529
529
  expected_length,
@@ -549,7 +549,7 @@ def list_all_log_events(log_group_name: str, logs_client=None) -> list[dict]:
549
549
  def get_lambda_log_events(
550
550
  function_name,
551
551
  delay_time=DEFAULT_GET_LOG_EVENTS_DELAY,
552
- regex_filter: Optional[str] = None,
552
+ regex_filter: str | None = None,
553
553
  log_group=None,
554
554
  logs_client=None,
555
555
  ):
@@ -597,7 +597,7 @@ def list_all_resources(
597
597
  page_function: Callable[[dict], Any],
598
598
  last_token_attr_name: str,
599
599
  list_attr_name: str,
600
- next_token_attr_name: Optional[str] = None,
600
+ next_token_attr_name: str | None = None,
601
601
  ) -> list:
602
602
  """
603
603
  List all available resources by loading all available pages using `page_function`.
@@ -3,9 +3,9 @@ import inspect
3
3
  import logging
4
4
  import threading
5
5
  import traceback
6
+ from collections.abc import Callable
6
7
  from concurrent.futures import Future
7
8
  from multiprocessing.dummy import Pool
8
- from typing import Callable, Optional
9
9
 
10
10
  LOG = logging.getLogger(__name__)
11
11
 
@@ -26,7 +26,7 @@ class FuncThread(threading.Thread):
26
26
  params=None,
27
27
  quiet=False,
28
28
  on_stop: Callable[["FuncThread"], None] = None,
29
- name: Optional[str] = None,
29
+ name: str | None = None,
30
30
  daemon=True,
31
31
  ):
32
32
  global counter
localstack/utils/time.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  from datetime import date, datetime, timezone, tzinfo
3
- from typing import Optional
3
+ from zoneinfo import ZoneInfo
4
4
 
5
5
  TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"
6
6
  TIMESTAMP_FORMAT_TZ = "%Y-%m-%dT%H:%M:%SZ"
@@ -42,6 +42,11 @@ def epoch_timestamp() -> float:
42
42
 
43
43
 
44
44
  def parse_timestamp(ts_str: str) -> datetime:
45
+ """
46
+ Parse the incoming date string into a timezone aware datetime object
47
+ :param ts_str:
48
+ :return:
49
+ """
45
50
  for ts_format in [
46
51
  TIMESTAMP_FORMAT,
47
52
  TIMESTAMP_FORMAT_TZ,
@@ -49,13 +54,16 @@ def parse_timestamp(ts_str: str) -> datetime:
49
54
  TIMESTAMP_READABLE_FORMAT,
50
55
  ]:
51
56
  try:
52
- return datetime.strptime(ts_str, ts_format)
57
+ value = datetime.strptime(ts_str, ts_format)
58
+ if value.tzinfo is None:
59
+ value = value.replace(tzinfo=ZoneInfo("UTC"))
60
+ return value
53
61
  except ValueError:
54
62
  pass
55
- raise Exception("Unable to parse timestamp string with any known formats: %s" % ts_str)
63
+ raise Exception(f"Unable to parse timestamp string with any known formats: {ts_str}")
56
64
 
57
65
 
58
- def now(millis: bool = False, tz: Optional[tzinfo] = None) -> int:
66
+ def now(millis: bool = False, tz: tzinfo | None = None) -> int:
59
67
  return mktime(datetime.now(tz=tz), millis=millis)
60
68
 
61
69
 
localstack/utils/urls.py CHANGED
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from localstack import config
4
2
  from localstack.config import HostAndPort
5
3
 
@@ -12,7 +10,7 @@ def hostname_from_url(url: str) -> str:
12
10
  return url.split("://")[-1].split("/")[0].split(":")[0]
13
11
 
14
12
 
15
- def localstack_host(custom_port: Optional[int] = None) -> HostAndPort:
13
+ def localstack_host(custom_port: int | None = None) -> HostAndPort:
16
14
  """
17
15
  Determine the host and port to return to the user based on:
18
16
  - the user's configuration (e.g environment variable overrides)
@@ -29,7 +29,7 @@ class TraceId:
29
29
  """
30
30
  Convert TraceId object to a string.
31
31
  """
32
- return "%s%s%s%s%s" % (
32
+ return "{}{}{}{}{}".format(
33
33
  TraceId.VERSION,
34
34
  TraceId.DELIMITER,
35
35
  format(self.start_time, "x"),
localstack/version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '4.7.1.dev49'
21
- __version_tuple__ = version_tuple = (4, 7, 1, 'dev49')
31
+ __version__ = version = '4.10.1.dev12'
32
+ __version_tuple__ = version_tuple = (4, 10, 1, 'dev12')
33
+
34
+ __commit_id__ = commit_id = None