localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__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 (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
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
 
@@ -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
@@ -275,7 +275,7 @@ def get_free_tcp_port_range(num_ports: int, max_attempts: int = 50) -> "PortRang
275
275
  raise PortNotAvailableException("reached max_attempts when trying to find port range")
276
276
 
277
277
 
278
- def resolve_hostname(hostname: str) -> Optional[str]:
278
+ def resolve_hostname(hostname: str) -> str | None:
279
279
  """Resolve the given hostname and return its IP address, or None if it cannot be resolved."""
280
280
  try:
281
281
  return socket.gethostbyname(hostname)
@@ -362,7 +362,7 @@ class PortRange:
362
362
  """
363
363
  return range(self.start, self.end + 1)
364
364
 
365
- 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:
366
366
  """
367
367
  Reserves the given port (if it is still free). If the given port is None, it reserves a free port from the
368
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
 
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):
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:
@@ -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 {}
@@ -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()
@@ -46,7 +45,7 @@ class Server(abc.ABC):
46
45
  def url(self):
47
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
@@ -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
 
@@ -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,6 +1,3 @@
1
- from typing import Optional
2
-
3
-
4
1
  class TaggingService:
5
2
  key_field: str
6
3
  value_field: str
@@ -17,7 +14,7 @@ class TaggingService:
17
14
 
18
15
  self.tags = {}
19
16
 
20
- 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):
21
18
  root_name = root_name or "Tags"
22
19
 
23
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
@@ -548,7 +549,7 @@ def list_all_log_events(log_group_name: str, logs_client=None) -> list[dict]:
548
549
  def get_lambda_log_events(
549
550
  function_name,
550
551
  delay_time=DEFAULT_GET_LOG_EVENTS_DELAY,
551
- regex_filter: Optional[str] = None,
552
+ regex_filter: str | None = None,
552
553
  log_group=None,
553
554
  logs_client=None,
554
555
  ):
@@ -596,7 +597,7 @@ def list_all_resources(
596
597
  page_function: Callable[[dict], Any],
597
598
  last_token_attr_name: str,
598
599
  list_attr_name: str,
599
- next_token_attr_name: Optional[str] = None,
600
+ next_token_attr_name: str | None = None,
600
601
  ) -> list:
601
602
  """
602
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