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.
- localstack/aws/api/cloudformation/__init__.py +1 -0
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/ec2/__init__.py +1113 -56
- localstack/aws/api/iam/__init__.py +7 -0
- localstack/aws/api/kinesis/__init__.py +19 -0
- localstack/aws/api/kms/__init__.py +6 -0
- localstack/aws/api/lambda_/__init__.py +13 -0
- localstack/aws/api/logs/__init__.py +15 -0
- localstack/aws/api/redshift/__init__.py +9 -3
- localstack/aws/api/route53/__init__.py +2 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +32 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +32 -9
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +33 -13
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +4 -4
- localstack/cli/lpm.py +3 -4
- localstack/cli/profiles.py +1 -2
- localstack/config.py +18 -12
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +1 -1
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +12 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/init.py +1 -1
- localstack/services/apigateway/legacy/provider.py +53 -3
- localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
- localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/cloudformation/engine/entities.py +12 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +6 -4
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/resource_provider.py +5 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/v2/entities.py +6 -3
- localstack/services/cloudformation/v2/provider.py +172 -27
- localstack/services/cloudformation/v2/types.py +8 -4
- localstack/services/cloudwatch/provider_v2.py +25 -28
- localstack/services/dynamodb/packages.py +2 -1
- localstack/services/dynamodb/provider.py +42 -0
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/es/provider.py +2 -2
- localstack/services/events/event_rule_engine.py +31 -13
- localstack/services/events/models.py +4 -5
- localstack/services/events/target.py +17 -9
- localstack/services/iam/provider.py +11 -116
- localstack/services/iam/resources/policy_simulator.py +133 -0
- localstack/services/kinesis/models.py +15 -2
- localstack/services/kinesis/provider.py +77 -0
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/logs/provider.py +1 -1
- localstack/services/moto.py +2 -1
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +27 -43
- localstack/services/s3/provider.py +67 -11
- localstack/services/s3/utils.py +42 -11
- localstack/services/ses/provider.py +16 -7
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/v2/models.py +167 -0
- localstack/services/sns/v2/provider.py +860 -2
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +42 -3
- localstack/services/sqs/provider.py +8 -309
- localstack/services/sqs/query_api.py +1 -1
- localstack/services/sqs/utils.py +121 -2
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +20 -19
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +13 -4
- localstack/testing/pytest/stepfunctions/utils.py +4 -3
- localstack/testing/pytest/util.py +1 -1
- localstack/testing/pytest/validation_tracking.py +1 -2
- localstack/testing/snapshots/transformer_utility.py +5 -0
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +1 -2
- localstack/utils/analytics/metrics/counter.py +6 -8
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/aws/arns.py +17 -9
- localstack/utils/aws/aws_responses.py +7 -7
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +7 -7
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +11 -0
- localstack/utils/catalog/common.py +58 -0
- localstack/utils/catalog/plugins.py +28 -0
- localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
- localstack/utils/collections.py +7 -8
- localstack/utils/config_listener.py +1 -1
- localstack/utils/container_networking.py +2 -3
- localstack/utils/container_utils/container_client.py +115 -131
- localstack/utils/container_utils/docker_cmd_client.py +42 -42
- localstack/utils/container_utils/docker_sdk_client.py +63 -62
- localstack/utils/diagnose.py +2 -3
- localstack/utils/docker_utils.py +3 -4
- localstack/utils/files.py +31 -7
- localstack/utils/functions.py +3 -2
- localstack/utils/http.py +4 -5
- localstack/utils/json.py +19 -5
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +6 -6
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +6 -5
- localstack/utils/patch.py +2 -1
- localstack/utils/run.py +10 -9
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +2 -3
- localstack/utils/strings.py +10 -11
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +1 -4
- localstack/utils/testutil.py +5 -4
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +11 -3
- localstack/utils/urls.py +1 -3
- localstack/version.py +2 -2
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
- localstack/packages/terraform.py +0 -46
- localstack/services/cloudformation/deploy.html +0 -144
- localstack/services/cloudformation/deploy_ui.py +0 -47
- localstack/services/cloudformation/plugins.py +0 -12
- localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
|
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:
|
|
66
|
+
path: str | os.PathLike
|
|
67
67
|
|
|
68
|
-
def __init__(self, path:
|
|
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',
|
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
|
|
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 =
|
|
53
|
+
IntOrPort = int | Port
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def is_port_open(
|
|
57
|
-
port_or_url:
|
|
57
|
+
port_or_url: int | str,
|
|
58
58
|
http_path: str = None,
|
|
59
59
|
expect_success: bool = True,
|
|
60
|
-
protocols:
|
|
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) ->
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
localstack/utils/numbers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
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) ->
|
|
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))
|
localstack/utils/objects.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import re
|
|
3
3
|
import threading
|
|
4
|
-
from
|
|
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 =
|
|
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:
|
|
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) ->
|
|
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:
|
|
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
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
|
|
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:
|
|
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:
|
|
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
|
-
) ->
|
|
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:
|
|
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:
|
|
215
|
+
cmd: str | list[str],
|
|
215
216
|
params: Any = None,
|
|
216
|
-
outfile:
|
|
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:
|
|
227
|
-
cwd:
|
|
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 {}
|
localstack/utils/scheduler.py
CHANGED
|
@@ -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
|
|
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:
|
|
17
|
+
period: float | None = None,
|
|
18
18
|
fixed_rate: bool = True,
|
|
19
|
-
start:
|
|
19
|
+
start: float | None = None,
|
|
20
20
|
on_error: Callable[[Exception], None] = None,
|
|
21
|
-
args:
|
|
22
|
-
kwargs:
|
|
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:
|
|
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:
|
|
97
|
+
period: float | None = None,
|
|
98
98
|
fixed_rate: bool = True,
|
|
99
|
-
start:
|
|
99
|
+
start: float | None = None,
|
|
100
100
|
on_error: Callable[[Exception], None] = None,
|
|
101
|
-
args:
|
|
102
|
-
kwargs:
|
|
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
|
|
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
|
localstack/utils/serving.py
CHANGED
|
@@ -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:
|
|
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) ->
|
|
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
|
"""
|
localstack/utils/strings.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
localstack/utils/tagging.py
CHANGED
|
@@ -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:
|
|
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 = []
|
localstack/utils/testutil.py
CHANGED
|
@@ -7,7 +7,8 @@ import re
|
|
|
7
7
|
import shutil
|
|
8
8
|
import tempfile
|
|
9
9
|
import time
|
|
10
|
-
from
|
|
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
|
|
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:
|
|
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:
|
|
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`.
|
localstack/utils/threads.py
CHANGED
|
@@ -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:
|
|
29
|
+
name: str | None = None,
|
|
30
30
|
daemon=True,
|
|
31
31
|
):
|
|
32
32
|
global counter
|