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.
- localstack/aws/api/cloudformation/__init__.py +18 -4
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +6 -2
- localstack/aws/api/dynamodb/__init__.py +30 -0
- localstack/aws/api/ec2/__init__.py +1522 -65
- 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 +5 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +54 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- localstack/aws/api/transcribe/__init__.py +17 -0
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/internal_requests.py +6 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +40 -20
- localstack/aws/mocking.py +2 -2
- localstack/aws/patches.py +2 -2
- localstack/aws/protocol/parser.py +459 -32
- localstack/aws/protocol/serializer.py +689 -69
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/protocol/validate.py +1 -1
- localstack/aws/scaffold.py +1 -1
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +37 -16
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +6 -6
- localstack/cli/lpm.py +3 -4
- localstack/cli/plugins.py +1 -1
- localstack/cli/profiles.py +1 -2
- localstack/config.py +25 -18
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +130 -7
- localstack/dev/run/configurators.py +1 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +13 -4
- localstack/logging/format.py +3 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/analytics.py +3 -0
- localstack/runtime/hooks.py +1 -1
- localstack/runtime/init.py +2 -2
- localstack/runtime/main.py +5 -5
- localstack/runtime/patches.py +2 -2
- localstack/services/apigateway/helpers.py +1 -4
- localstack/services/apigateway/legacy/helpers.py +7 -8
- localstack/services/apigateway/legacy/integration.py +4 -3
- localstack/services/apigateway/legacy/invocations.py +6 -5
- localstack/services/apigateway/legacy/provider.py +148 -68
- localstack/services/apigateway/legacy/templates.py +1 -1
- localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
- localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
- 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/template_mapping.py +2 -2
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
- localstack/services/cloudformation/api_utils.py +4 -8
- localstack/services/cloudformation/cfn_utils.py +1 -1
- localstack/services/cloudformation/engine/entities.py +14 -4
- localstack/services/cloudformation/engine/template_deployer.py +6 -4
- localstack/services/cloudformation/engine/transformers.py +6 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +7 -5
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +7 -5
- localstack/services/cloudformation/resource_provider.py +7 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/service_models.py +2 -2
- localstack/services/cloudformation/v2/entities.py +19 -9
- localstack/services/cloudformation/v2/provider.py +336 -106
- localstack/services/cloudformation/v2/types.py +13 -7
- localstack/services/cloudformation/v2/utils.py +4 -1
- localstack/services/cloudwatch/alarm_scheduler.py +4 -1
- localstack/services/cloudwatch/provider.py +18 -13
- 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/server.py +2 -2
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/edge.py +1 -1
- 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/provider.py +17 -14
- localstack/services/events/target.py +17 -9
- localstack/services/events/v1/provider.py +5 -5
- localstack/services/firehose/provider.py +14 -4
- 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 +86 -3
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/api_utils.py +6 -3
- localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
- localstack/services/lambda_/invocation/event_manager.py +1 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/invocation/lambda_models.py +10 -7
- localstack/services/lambda_/invocation/lambda_service.py +5 -1
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +4 -3
- localstack/services/lambda_/provider_utils.py +1 -1
- localstack/services/logs/provider.py +36 -19
- 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 +8 -2
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/plugins.py +11 -7
- localstack/services/providers.py +10 -2
- localstack/services/redshift/provider.py +0 -21
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/models.py +1 -1
- localstack/services/s3/notifications.py +55 -39
- localstack/services/s3/presigned_url.py +35 -54
- localstack/services/s3/provider.py +73 -15
- localstack/services/s3/utils.py +42 -22
- localstack/services/s3/validation.py +46 -32
- localstack/services/s3/website_hosting.py +4 -2
- localstack/services/ses/provider.py +18 -8
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/executor.py +9 -2
- localstack/services/sns/provider.py +8 -5
- localstack/services/sns/publisher.py +31 -16
- localstack/services/sns/v2/models.py +167 -0
- localstack/services/sns/v2/provider.py +867 -0
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/constants.py +1 -1
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +48 -5
- localstack/services/sqs/provider.py +38 -311
- localstack/services/sqs/query_api.py +6 -2
- localstack/services/sqs/utils.py +121 -2
- localstack/services/ssm/provider.py +1 -1
- localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
- localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
- localstack/services/stepfunctions/asl/eval/environment.py +1 -1
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/services/stepfunctions/backend/execution.py +2 -1
- localstack/services/stores.py +1 -1
- localstack/services/transcribe/provider.py +6 -1
- localstack/state/codecs.py +61 -0
- localstack/state/core.py +11 -5
- localstack/state/pickle.py +10 -49
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/cloudformation/transformers.py +0 -0
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +33 -31
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +38 -11
- 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 +6 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +6 -4
- localstack/utils/analytics/metrics/counter.py +8 -15
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_providers.py +19 -0
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/asyncio.py +2 -2
- localstack/utils/aws/arns.py +24 -29
- localstack/utils/aws/aws_responses.py +8 -8
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/dead_letter_queue.py +1 -5
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/aws/resources.py +1 -1
- localstack/utils/aws/templating.py +1 -1
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +21 -13
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +119 -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 +135 -136
- localstack/utils/container_utils/docker_cmd_client.py +85 -69
- localstack/utils/container_utils/docker_sdk_client.py +69 -66
- localstack/utils/crypto.py +10 -10
- localstack/utils/diagnose.py +3 -4
- localstack/utils/docker_utils.py +9 -5
- localstack/utils/files.py +33 -13
- localstack/utils/functions.py +4 -3
- localstack/utils/http.py +11 -11
- localstack/utils/json.py +20 -6
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +15 -9
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +7 -6
- localstack/utils/patch.py +10 -3
- localstack/utils/run.py +12 -11
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +3 -4
- localstack/utils/strings.py +15 -16
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +8 -6
- localstack/utils/testutil.py +8 -8
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +12 -4
- localstack/utils/urls.py +1 -3
- localstack/utils/xray/traceid.py +1 -1
- localstack/version.py +16 -3
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
- localstack_core-4.10.1.dev12.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.dev49.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
localstack/utils/crypto.py
CHANGED
|
@@ -43,8 +43,8 @@ def generate_ssl_cert(
|
|
|
43
43
|
return all(os.path.exists(f) for f in files)
|
|
44
44
|
|
|
45
45
|
def store_cert_key_files(base_filename):
|
|
46
|
-
key_file_name = "
|
|
47
|
-
cert_file_name = "
|
|
46
|
+
key_file_name = f"{base_filename}.key"
|
|
47
|
+
cert_file_name = f"{base_filename}.crt"
|
|
48
48
|
# TODO: Cleaner code to load the cert dynamically
|
|
49
49
|
# extract key and cert from target_file and store into separate files
|
|
50
50
|
content = load_file(target_file)
|
|
@@ -74,9 +74,9 @@ def generate_ssl_cert(
|
|
|
74
74
|
return target_file, cert_file_name, key_file_name
|
|
75
75
|
if random and target_file:
|
|
76
76
|
if "." in target_file:
|
|
77
|
-
target_file = target_file.replace(".", "
|
|
77
|
+
target_file = target_file.replace(".", f".{short_uid()}.", 1)
|
|
78
78
|
else:
|
|
79
|
-
target_file = "
|
|
79
|
+
target_file = f"{target_file}.{short_uid()}"
|
|
80
80
|
|
|
81
81
|
# create a key pair
|
|
82
82
|
k = crypto.PKey()
|
|
@@ -123,10 +123,10 @@ def generate_ssl_cert(
|
|
|
123
123
|
key_file.write(to_str(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)))
|
|
124
124
|
cert_file_content = cert_file.getvalue().strip()
|
|
125
125
|
key_file_content = key_file.getvalue().strip()
|
|
126
|
-
file_content = "
|
|
126
|
+
file_content = f"{key_file_content}\n{cert_file_content}"
|
|
127
127
|
if target_file:
|
|
128
|
-
key_file_name = "
|
|
129
|
-
cert_file_name = "
|
|
128
|
+
key_file_name = f"{target_file}.key"
|
|
129
|
+
cert_file_name = f"{target_file}.crt"
|
|
130
130
|
# check existence to avoid permission denied issues:
|
|
131
131
|
# https://github.com/localstack/localstack/issues/1607
|
|
132
132
|
if not all_exist(target_file, key_file_name, cert_file_name):
|
|
@@ -145,9 +145,9 @@ def generate_ssl_cert(
|
|
|
145
145
|
e,
|
|
146
146
|
)
|
|
147
147
|
# Fix for https://github.com/localstack/localstack/issues/1743
|
|
148
|
-
target_file = "
|
|
149
|
-
key_file_name = "
|
|
150
|
-
cert_file_name = "
|
|
148
|
+
target_file = f"{new_tmp_file()}.pem"
|
|
149
|
+
key_file_name = f"{target_file}.key"
|
|
150
|
+
cert_file_name = f"{target_file}.crt"
|
|
151
151
|
TMP_FILES.append(target_file)
|
|
152
152
|
TMP_FILES.append(key_file_name)
|
|
153
153
|
TMP_FILES.append(cert_file_name)
|
localstack/utils/diagnose.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
5
|
import socket
|
|
6
|
-
from typing import Optional, Union
|
|
7
6
|
|
|
8
7
|
from localstack import config
|
|
9
8
|
from localstack.constants import DEFAULT_VOLUME_DIR
|
|
@@ -77,14 +76,14 @@ def get_localstack_config() -> dict:
|
|
|
77
76
|
return result
|
|
78
77
|
|
|
79
78
|
|
|
80
|
-
def inspect_main_container() ->
|
|
79
|
+
def inspect_main_container() -> str | dict:
|
|
81
80
|
try:
|
|
82
81
|
return DOCKER_CLIENT.inspect_container(get_main_container_name())
|
|
83
82
|
except Exception as e:
|
|
84
83
|
return f"inspect failed: {e}"
|
|
85
84
|
|
|
86
85
|
|
|
87
|
-
def get_localstack_version() -> dict[str,
|
|
86
|
+
def get_localstack_version() -> dict[str, str | None]:
|
|
88
87
|
return {
|
|
89
88
|
"build-date": os.environ.get("LOCALSTACK_BUILD_DATE"),
|
|
90
89
|
"build-git-hash": os.environ.get("LOCALSTACK_BUILD_GIT_HASH"),
|
|
@@ -134,7 +133,7 @@ def traverse_file_tree(root: str) -> list[str]:
|
|
|
134
133
|
result.append(dirpath)
|
|
135
134
|
return result
|
|
136
135
|
except Exception as e:
|
|
137
|
-
return ["traversing files failed
|
|
136
|
+
return [f"traversing files failed {e}"]
|
|
138
137
|
|
|
139
138
|
|
|
140
139
|
def get_docker_image_details() -> dict[str, str]:
|
localstack/utils/docker_utils.py
CHANGED
|
@@ -2,13 +2,13 @@ import functools
|
|
|
2
2
|
import logging
|
|
3
3
|
import platform
|
|
4
4
|
import random
|
|
5
|
-
from typing import Optional, Union
|
|
6
5
|
|
|
7
6
|
from localstack import config
|
|
8
7
|
from localstack.constants import DEFAULT_VOLUME_DIR, DOCKER_IMAGE_NAME
|
|
9
8
|
from localstack.utils.collections import ensure_list
|
|
10
9
|
from localstack.utils.container_utils.container_client import (
|
|
11
10
|
ContainerClient,
|
|
11
|
+
DockerNotAvailable,
|
|
12
12
|
PortMappings,
|
|
13
13
|
VolumeInfo,
|
|
14
14
|
)
|
|
@@ -79,7 +79,7 @@ def inspect_current_container_mounts() -> list[VolumeInfo]:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
@functools.lru_cache
|
|
82
|
-
def get_default_volume_dir_mount() ->
|
|
82
|
+
def get_default_volume_dir_mount() -> VolumeInfo | None:
|
|
83
83
|
"""
|
|
84
84
|
Returns the volume information of LocalStack's DEFAULT_VOLUME_DIR (/var/lib/localstack), if mounted,
|
|
85
85
|
else it returns None. If we're not currently in docker a VauleError is raised. in a container, a ValueError is
|
|
@@ -132,8 +132,8 @@ def get_host_path_for_path_in_docker(path):
|
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
def container_ports_can_be_bound(
|
|
135
|
-
ports:
|
|
136
|
-
address:
|
|
135
|
+
ports: IntOrPort | list[IntOrPort],
|
|
136
|
+
address: str | None = None,
|
|
137
137
|
) -> bool:
|
|
138
138
|
"""Determine whether a given list of ports can be bound by Docker containers
|
|
139
139
|
|
|
@@ -153,10 +153,14 @@ def container_ports_can_be_bound(
|
|
|
153
153
|
ports=port_mappings,
|
|
154
154
|
remove=True,
|
|
155
155
|
)
|
|
156
|
+
except DockerNotAvailable as e:
|
|
157
|
+
LOG.warning("Cannot perform port check because Docker is not available.")
|
|
158
|
+
raise e
|
|
156
159
|
except Exception as e:
|
|
157
160
|
if "port is already allocated" not in str(e) and "address already in use" not in str(e):
|
|
158
161
|
LOG.warning(
|
|
159
|
-
"Unexpected error when attempting to determine container port status",
|
|
162
|
+
"Unexpected error when attempting to determine container port status",
|
|
163
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
160
164
|
)
|
|
161
165
|
return False
|
|
162
166
|
# TODO(srw): sometimes the command output from the docker container is "None", particularly when this function is
|
localstack/utils/files.py
CHANGED
|
@@ -80,9 +80,26 @@ def save_file(file, content, append=False, permissions=None):
|
|
|
80
80
|
f.flush()
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def load_file(
|
|
83
|
+
def load_file(
|
|
84
|
+
file_path: str | os.PathLike,
|
|
85
|
+
default: str | bytes | None = None,
|
|
86
|
+
mode: str | None = None,
|
|
87
|
+
strict: bool = False,
|
|
88
|
+
) -> str | bytes | None:
|
|
89
|
+
"""
|
|
90
|
+
Return file contents
|
|
91
|
+
|
|
92
|
+
:param file_path: path of the file
|
|
93
|
+
:param default: if strict=False then return this value if the file does not exist
|
|
94
|
+
:param mode: mode to open the file with (e.g. `r`, `rw`)
|
|
95
|
+
:param strict: raise an error if the file path is not a file
|
|
96
|
+
:return: the file contents
|
|
97
|
+
"""
|
|
84
98
|
if not os.path.isfile(file_path):
|
|
85
|
-
|
|
99
|
+
if strict:
|
|
100
|
+
raise FileNotFoundError(file_path)
|
|
101
|
+
else:
|
|
102
|
+
return default
|
|
86
103
|
if not mode:
|
|
87
104
|
mode = "r"
|
|
88
105
|
with open(file_path, mode) as f:
|
|
@@ -201,7 +218,7 @@ def rm_rf(path: str):
|
|
|
201
218
|
# Running the native command can be an order of magnitude faster in Alpine on Travis-CI
|
|
202
219
|
if is_debian():
|
|
203
220
|
try:
|
|
204
|
-
return run('rm -rf "
|
|
221
|
+
return run(f'rm -rf "{path}"')
|
|
205
222
|
except Exception:
|
|
206
223
|
pass
|
|
207
224
|
# Make sure all files are writeable and dirs executable to remove
|
|
@@ -247,11 +264,7 @@ def cp_r(src: str, dst: str, rm_dest_on_conflict=False, ignore_copystat_errors=F
|
|
|
247
264
|
except Exception as e:
|
|
248
265
|
|
|
249
266
|
def _info(_path):
|
|
250
|
-
return "
|
|
251
|
-
_path,
|
|
252
|
-
os.path.isfile(_path),
|
|
253
|
-
os.path.islink(_path),
|
|
254
|
-
)
|
|
267
|
+
return f"{_path} (file={os.path.isfile(_path)}, symlink={os.path.islink(_path)})"
|
|
255
268
|
|
|
256
269
|
LOG.debug("Error copying files from %s to %s: %s", _info(src), _info(dst), e)
|
|
257
270
|
raise
|
|
@@ -292,7 +305,7 @@ def cleanup_tmp_files():
|
|
|
292
305
|
del TMP_FILES[:]
|
|
293
306
|
|
|
294
307
|
|
|
295
|
-
def new_tmp_file(suffix: str = None, dir: str = None) -> str:
|
|
308
|
+
def new_tmp_file(suffix: str | None = None, dir: str | None = None) -> str:
|
|
296
309
|
"""Return a path to a new temporary file."""
|
|
297
310
|
tmp_file, tmp_path = tempfile.mkstemp(suffix=suffix, dir=dir)
|
|
298
311
|
os.close(tmp_file)
|
|
@@ -300,8 +313,15 @@ def new_tmp_file(suffix: str = None, dir: str = None) -> str:
|
|
|
300
313
|
return tmp_path
|
|
301
314
|
|
|
302
315
|
|
|
303
|
-
def new_tmp_dir(dir: str = None):
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
316
|
+
def new_tmp_dir(dir: str | None = None, mode: int = 0o777) -> str:
|
|
317
|
+
"""
|
|
318
|
+
Create a new temporary directory with the specified permissions. The directory is added to the tracked temporary
|
|
319
|
+
files.
|
|
320
|
+
:param dir: parent directory for the temporary directory to be created. Systems's default otherwise.
|
|
321
|
+
:param mode: file permission for the directory (default: 0o777)
|
|
322
|
+
:return: the absolute path of the created directory
|
|
323
|
+
"""
|
|
324
|
+
folder = tempfile.mkdtemp(dir=dir)
|
|
325
|
+
TMP_FILES.append(folder)
|
|
326
|
+
idempotent_chmod(folder, mode=mode)
|
|
307
327
|
return folder
|
localstack/utils/functions.py
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import functools
|
|
4
4
|
import inspect
|
|
5
5
|
import logging
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
LOG = logging.getLogger(__name__)
|
|
9
10
|
|
|
@@ -20,7 +21,7 @@ def run_safe(_python_lambda, *args, _default=None, **kwargs):
|
|
|
20
21
|
|
|
21
22
|
def call_safe(
|
|
22
23
|
func: Callable, args: tuple = None, kwargs: dict = None, exception_message: str = None
|
|
23
|
-
) ->
|
|
24
|
+
) -> Any | None:
|
|
24
25
|
"""
|
|
25
26
|
Call the given function with the given arguments, and if it fails, log the given exception_message.
|
|
26
27
|
If logging.DEBUG is set for the logger, then we also log the traceback.
|
|
@@ -32,7 +33,7 @@ def call_safe(
|
|
|
32
33
|
:return: whatever the func returns
|
|
33
34
|
"""
|
|
34
35
|
if exception_message is None:
|
|
35
|
-
exception_message = "error calling function
|
|
36
|
+
exception_message = f"error calling function {func.__name__}"
|
|
36
37
|
if args is None:
|
|
37
38
|
args = ()
|
|
38
39
|
if kwargs is None:
|
localstack/utils/http.py
CHANGED
|
@@ -2,7 +2,6 @@ import logging
|
|
|
2
2
|
import math
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
-
from typing import Optional, Union
|
|
6
5
|
from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse
|
|
7
6
|
|
|
8
7
|
import requests
|
|
@@ -43,18 +42,18 @@ def create_chunked_data(data, chunk_size: int = 80):
|
|
|
43
42
|
dl = len(data)
|
|
44
43
|
ret = ""
|
|
45
44
|
for i in range(dl // chunk_size):
|
|
46
|
-
ret += "
|
|
47
|
-
ret += "
|
|
45
|
+
ret += f"{hex(chunk_size)[2:]}\r\n"
|
|
46
|
+
ret += f"{data[i * chunk_size : (i + 1) * chunk_size]}\r\n\r\n"
|
|
48
47
|
|
|
49
48
|
if len(data) % chunk_size != 0:
|
|
50
|
-
ret += "
|
|
51
|
-
ret += "
|
|
49
|
+
ret += f"{hex(len(data) % chunk_size)[2:]}\r\n"
|
|
50
|
+
ret += f"{data[-(len(data) % chunk_size) :]}\r\n"
|
|
52
51
|
|
|
53
52
|
ret += "0\r\n\r\n"
|
|
54
53
|
return ret
|
|
55
54
|
|
|
56
55
|
|
|
57
|
-
def canonicalize_headers(headers:
|
|
56
|
+
def canonicalize_headers(headers: dict | CaseInsensitiveDict) -> dict:
|
|
58
57
|
if not headers:
|
|
59
58
|
return headers
|
|
60
59
|
|
|
@@ -103,7 +102,7 @@ def add_query_params_to_url(uri: str, query_params: dict) -> str:
|
|
|
103
102
|
|
|
104
103
|
|
|
105
104
|
def make_http_request(
|
|
106
|
-
url: str, data:
|
|
105
|
+
url: str, data: bytes | str = None, headers: dict[str, str] = None, method: str = "GET"
|
|
107
106
|
) -> Response:
|
|
108
107
|
return requests.request(
|
|
109
108
|
url=url, method=method, headers=headers, data=data, auth=NetrcBypassAuth(), verify=False
|
|
@@ -179,7 +178,7 @@ def download(
|
|
|
179
178
|
path: str,
|
|
180
179
|
verify_ssl: bool = True,
|
|
181
180
|
timeout: float = None,
|
|
182
|
-
request_headers:
|
|
181
|
+
request_headers: dict | None = None,
|
|
183
182
|
quiet: bool = False,
|
|
184
183
|
) -> None:
|
|
185
184
|
"""Downloads file at url to the given path. Raises TimeoutError if the optional timeout (in secs) is reached.
|
|
@@ -202,7 +201,7 @@ def download(
|
|
|
202
201
|
r = s.get(url, stream=True, verify=_verify, timeout=timeout, headers=request_headers)
|
|
203
202
|
# check status code before attempting to read body
|
|
204
203
|
if not r.ok:
|
|
205
|
-
raise Exception("Failed to download
|
|
204
|
+
raise Exception(f"Failed to download {url}, response code {r.status_code}")
|
|
206
205
|
|
|
207
206
|
total_size = 0
|
|
208
207
|
if r.headers.get("Content-Length"):
|
|
@@ -291,18 +290,19 @@ def download_github_artifact(url: str, target_file: str, timeout: int = None):
|
|
|
291
290
|
Optionally allows to define a timeout in seconds."""
|
|
292
291
|
|
|
293
292
|
def do_download(
|
|
294
|
-
download_url: str, request_headers:
|
|
293
|
+
download_url: str, request_headers: dict | None = None, print_error: bool = False
|
|
295
294
|
):
|
|
296
295
|
try:
|
|
297
296
|
download(download_url, target_file, timeout=timeout, request_headers=request_headers)
|
|
298
297
|
return True
|
|
299
298
|
except Exception as e:
|
|
300
299
|
if print_error:
|
|
301
|
-
LOG.
|
|
300
|
+
LOG.error(
|
|
302
301
|
"Unable to download Github artifact from %s to %s: %s %s",
|
|
303
302
|
url,
|
|
304
303
|
target_file,
|
|
305
304
|
e,
|
|
305
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
306
306
|
)
|
|
307
307
|
|
|
308
308
|
# if a GitHub API token is set, use it to avoid rate limiting issues
|
localstack/utils/json.py
CHANGED
|
@@ -5,7 +5,7 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
from datetime import date, datetime
|
|
7
7
|
from json import JSONDecodeError
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from localstack.config import HostAndPort
|
|
11
11
|
|
|
@@ -42,7 +42,7 @@ class CustomEncoder(json.JSONEncoder):
|
|
|
42
42
|
try:
|
|
43
43
|
if isinstance(o, bytes):
|
|
44
44
|
return to_str(o)
|
|
45
|
-
return super(
|
|
45
|
+
return super().default(o)
|
|
46
46
|
except Exception:
|
|
47
47
|
return None
|
|
48
48
|
|
|
@@ -63,9 +63,9 @@ class FileMappedDocument(dict):
|
|
|
63
63
|
concurrent writes, run load(). To save and overwrite the current document on disk, run save().
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
|
-
path:
|
|
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
|
|
@@ -97,7 +97,12 @@ def is_port_open(
|
|
|
97
97
|
sock.recvfrom(1024)
|
|
98
98
|
except Exception:
|
|
99
99
|
if not quiet:
|
|
100
|
-
LOG.
|
|
100
|
+
LOG.error(
|
|
101
|
+
"Error connecting to UDP port %s:%s",
|
|
102
|
+
host,
|
|
103
|
+
port,
|
|
104
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
105
|
+
)
|
|
101
106
|
return False
|
|
102
107
|
elif nw_protocol == socket.SOCK_STREAM:
|
|
103
108
|
result = sock.connect_ex((host, port))
|
|
@@ -159,8 +164,9 @@ def wait_for_port_status(
|
|
|
159
164
|
status = is_port_open(port, http_path=http_path, expect_success=expect_success)
|
|
160
165
|
if bool(status) != (not expect_closed):
|
|
161
166
|
raise Exception(
|
|
162
|
-
"Port
|
|
163
|
-
|
|
167
|
+
"Port {} (path: {}) was not {}".format(
|
|
168
|
+
port, http_path, "closed" if expect_closed else "open"
|
|
169
|
+
)
|
|
164
170
|
)
|
|
165
171
|
|
|
166
172
|
return retry(check, sleep=sleep_time, retries=retries)
|
|
@@ -269,7 +275,7 @@ def get_free_tcp_port_range(num_ports: int, max_attempts: int = 50) -> "PortRang
|
|
|
269
275
|
raise PortNotAvailableException("reached max_attempts when trying to find port range")
|
|
270
276
|
|
|
271
277
|
|
|
272
|
-
def resolve_hostname(hostname: str) ->
|
|
278
|
+
def resolve_hostname(hostname: str) -> str | None:
|
|
273
279
|
"""Resolve the given hostname and return its IP address, or None if it cannot be resolved."""
|
|
274
280
|
try:
|
|
275
281
|
return socket.gethostbyname(hostname)
|
|
@@ -356,7 +362,7 @@ class PortRange:
|
|
|
356
362
|
"""
|
|
357
363
|
return range(self.start, self.end + 1)
|
|
358
364
|
|
|
359
|
-
def reserve_port(self, port:
|
|
365
|
+
def reserve_port(self, port: IntOrPort | None = None, duration: int | None = None) -> int:
|
|
360
366
|
"""
|
|
361
367
|
Reserves the given port (if it is still free). If the given port is None, it reserves a free port from the
|
|
362
368
|
configured port range for external services. If a port is given, it has to be within the configured
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
|
-
from typing import NoReturn
|
|
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
|
|
|
@@ -163,7 +164,7 @@ def keys_to(
|
|
|
163
164
|
skip_children_of = ensure_list(skip_children_of or [])
|
|
164
165
|
|
|
165
166
|
def fix_keys(o, path="", **kwargs):
|
|
166
|
-
if any(re.match(
|
|
167
|
+
if any(re.match(rf"(^|.*\.){k}($|[.\[].*)", path) for k in skip_children_of):
|
|
167
168
|
return o
|
|
168
169
|
if isinstance(o, dict):
|
|
169
170
|
for k, v in dict(o).items():
|
localstack/utils/patch.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import inspect
|
|
3
3
|
import types
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def get_defining_object(method):
|
|
@@ -97,19 +98,25 @@ class Patch:
|
|
|
97
98
|
self.is_applied = False
|
|
98
99
|
|
|
99
100
|
def apply(self):
|
|
101
|
+
if self.is_applied:
|
|
102
|
+
return
|
|
103
|
+
|
|
100
104
|
if self.old and self.name == "__getattr__":
|
|
101
105
|
raise Exception("You can't patch class types implementing __getattr__")
|
|
102
106
|
if not self.old and self.name != "__getattr__":
|
|
103
107
|
raise AttributeError(f"`{self.obj.__name__}` object has no attribute `{self.name}`")
|
|
104
108
|
setattr(self.obj, self.name, self.new)
|
|
105
|
-
self.is_applied = True
|
|
106
109
|
Patch.applied_patches.append(self)
|
|
110
|
+
self.is_applied = True
|
|
107
111
|
|
|
108
112
|
def undo(self):
|
|
113
|
+
if not self.is_applied:
|
|
114
|
+
return
|
|
115
|
+
|
|
109
116
|
# If we added a method to a class type, we don't have a self.old. We just delete __getattr__
|
|
110
117
|
setattr(self.obj, self.name, self.old) if self.old else delattr(self.obj, self.name)
|
|
111
|
-
self.is_applied = False
|
|
112
118
|
Patch.applied_patches.remove(self)
|
|
119
|
+
self.is_applied = False
|
|
113
120
|
|
|
114
121
|
def __enter__(self):
|
|
115
122
|
self.apply()
|