localstack-core 4.9.3.dev43__py3-none-any.whl → 4.9.3.dev57__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.
Potentially problematic release.
This version of localstack-core might be problematic. Click here for more details.
- localstack/aws/api/ec2/__init__.py +526 -46
- localstack/services/apigateway/legacy/provider.py +23 -1
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +0 -1
- localstack/services/cloudformation/v2/provider.py +6 -0
- localstack/services/s3/constants.py +0 -2
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +13 -28
- localstack/services/s3/provider.py +4 -4
- localstack/services/s3/utils.py +2 -11
- localstack/services/sns/v2/provider.py +61 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/utils/files.py +31 -7
- localstack/utils/json.py +16 -2
- localstack/version.py +2 -2
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/METADATA +14 -9
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/RECORD +25 -25
- localstack_core-4.9.3.dev57.dist-info/plux.json +1 -0
- localstack_core-4.9.3.dev43.dist-info/plux.json +0 -1
- {localstack_core-4.9.3.dev43.data → localstack_core-4.9.3.dev57.data}/scripts/localstack +0 -0
- {localstack_core-4.9.3.dev43.data → localstack_core-4.9.3.dev57.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.9.3.dev43.data → localstack_core-4.9.3.dev57.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/WHEEL +0 -0
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.9.3.dev43.dist-info → localstack_core-4.9.3.dev57.dist-info}/top_level.txt +0 -0
|
@@ -323,8 +323,18 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
323
323
|
tags: MapOfStringToString = None,
|
|
324
324
|
**kwargs,
|
|
325
325
|
) -> ApiKey:
|
|
326
|
+
if name and len(name) > 1024:
|
|
327
|
+
raise BadRequestException("Invalid API Key name, can be at most 1024 characters.")
|
|
328
|
+
if value:
|
|
329
|
+
if len(value) > 128:
|
|
330
|
+
raise BadRequestException("API Key value exceeds maximum size of 128 characters")
|
|
331
|
+
elif len(value) < 20:
|
|
332
|
+
raise BadRequestException("API Key value should be at least 20 characters")
|
|
333
|
+
if description and len(description) > 125000:
|
|
334
|
+
raise BadRequestException("Invalid API Key description specified.")
|
|
326
335
|
api_key = call_moto(context)
|
|
327
|
-
|
|
336
|
+
if name == "":
|
|
337
|
+
api_key.pop("name", None)
|
|
328
338
|
# transform array of stage keys [{'restApiId': '0iscapk09u', 'stageName': 'dev'}] into
|
|
329
339
|
# array of strings ['0iscapk09u/dev']
|
|
330
340
|
stage_keys = api_key.get("stageKeys", [])
|
|
@@ -2420,6 +2430,10 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2420
2430
|
for api_key in api_keys:
|
|
2421
2431
|
api_key.pop("value")
|
|
2422
2432
|
|
|
2433
|
+
if limit is not None:
|
|
2434
|
+
if limit < 1 or limit > 500:
|
|
2435
|
+
limit = None
|
|
2436
|
+
|
|
2423
2437
|
item_list = PaginatedList(api_keys)
|
|
2424
2438
|
|
|
2425
2439
|
def token_generator(item):
|
|
@@ -2444,6 +2458,14 @@ class ApigatewayProvider(ApigatewayApi, ServiceLifecycleHook):
|
|
|
2444
2458
|
patch_operations: ListOfPatchOperation = None,
|
|
2445
2459
|
**kwargs,
|
|
2446
2460
|
) -> ApiKey:
|
|
2461
|
+
for patch_op in patch_operations:
|
|
2462
|
+
if patch_op["path"] not in ("/description", "/enabled", "/name", "/customerId"):
|
|
2463
|
+
raise BadRequestException(
|
|
2464
|
+
f"Invalid patch path '{patch_op['path']}' specified for op '{patch_op['op']}'. Must be one of: [/description, /enabled, /name, /customerId]"
|
|
2465
|
+
)
|
|
2466
|
+
|
|
2467
|
+
if patch_op["path"] == "/description" and len(patch_op["value"]) > 125000:
|
|
2468
|
+
raise BadRequestException("Invalid API Key description specified.")
|
|
2447
2469
|
response: ApiKey = call_moto(context)
|
|
2448
2470
|
if "value" in response:
|
|
2449
2471
|
response.pop("value", None)
|
|
@@ -1109,6 +1109,12 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1109
1109
|
|
|
1110
1110
|
try:
|
|
1111
1111
|
resource = stack.resolved_resources[logical_resource_id]
|
|
1112
|
+
if resource.get("ResourceStatus") not in [
|
|
1113
|
+
StackStatus.CREATE_COMPLETE,
|
|
1114
|
+
StackStatus.UPDATE_COMPLETE,
|
|
1115
|
+
StackStatus.ROLLBACK_COMPLETE,
|
|
1116
|
+
]:
|
|
1117
|
+
raise KeyError
|
|
1112
1118
|
except KeyError:
|
|
1113
1119
|
raise ValidationError(
|
|
1114
1120
|
f"Resource {logical_resource_id} does not exist for stack {stack_name}"
|
|
@@ -10,8 +10,6 @@ from localstack.aws.api.s3 import (
|
|
|
10
10
|
)
|
|
11
11
|
from localstack.aws.api.s3 import Type as GranteeType
|
|
12
12
|
|
|
13
|
-
S3_VIRTUAL_HOST_FORWARDED_HEADER = "x-s3-vhost-forwarded-for"
|
|
14
|
-
|
|
15
13
|
S3_UPLOAD_PART_MIN_SIZE = 5242880
|
|
16
14
|
"""
|
|
17
15
|
This is minimum size allowed by S3 when uploading more than one part for a Multipart Upload, except for the last part
|
|
@@ -45,10 +45,8 @@ from localstack.services.s3.constants import (
|
|
|
45
45
|
SIGNATURE_V4_PARAMS,
|
|
46
46
|
)
|
|
47
47
|
from localstack.services.s3.utils import (
|
|
48
|
-
S3_VIRTUAL_HOST_FORWARDED_HEADER,
|
|
49
48
|
capitalize_header_name_from_snake_case,
|
|
50
49
|
extract_bucket_name_and_key_from_headers_and_path,
|
|
51
|
-
forwarded_from_virtual_host_addressed_request,
|
|
52
50
|
is_bucket_name_valid,
|
|
53
51
|
is_presigned_url_request,
|
|
54
52
|
uses_host_addressing,
|
|
@@ -567,34 +565,21 @@ class S3SigV4SignatureContext:
|
|
|
567
565
|
self._query_parameters
|
|
568
566
|
)
|
|
569
567
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
self.
|
|
577
|
-
|
|
578
|
-
|
|
568
|
+
netloc = urlparse.urlparse(self.request.url).netloc
|
|
569
|
+
self.host = netloc
|
|
570
|
+
self._original_host = netloc
|
|
571
|
+
if (host_addressed := uses_host_addressing(self._headers)) and not is_bucket_name_valid(
|
|
572
|
+
self._bucket
|
|
573
|
+
):
|
|
574
|
+
raise InvalidBucketName(BucketName=self._bucket)
|
|
575
|
+
|
|
576
|
+
if not host_addressed and not self.request.path.startswith(f"/{self._bucket}"):
|
|
577
|
+
# if in path style, check that the path starts with the bucket
|
|
578
|
+
# our path has been sanitized, we should use the un-sanitized one
|
|
579
579
|
splitted_path = self.request.path.split("/", maxsplit=2)
|
|
580
|
-
self.path = f"/{splitted_path[-1]}"
|
|
581
|
-
|
|
580
|
+
self.path = f"/{self._bucket}/{splitted_path[-1]}"
|
|
582
581
|
else:
|
|
583
|
-
|
|
584
|
-
self.host = netloc
|
|
585
|
-
self._original_host = netloc
|
|
586
|
-
if (host_addressed := uses_host_addressing(self._headers)) and not is_bucket_name_valid(
|
|
587
|
-
self._bucket
|
|
588
|
-
):
|
|
589
|
-
raise InvalidBucketName(BucketName=self._bucket)
|
|
590
|
-
|
|
591
|
-
if not host_addressed and not self.request.path.startswith(f"/{self._bucket}"):
|
|
592
|
-
# if in path style, check that the path starts with the bucket
|
|
593
|
-
# our path has been sanitized, we should use the un-sanitized one
|
|
594
|
-
splitted_path = self.request.path.split("/", maxsplit=2)
|
|
595
|
-
self.path = f"/{self._bucket}/{splitted_path[-1]}"
|
|
596
|
-
else:
|
|
597
|
-
self.path = self.request.path
|
|
582
|
+
self.path = self.request.path
|
|
598
583
|
|
|
599
584
|
# we need to URL encode the path, as the key needs to be urlencoded for the signature to match
|
|
600
585
|
self.path = urlparse.quote(self.path)
|
|
@@ -384,7 +384,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
384
384
|
"""
|
|
385
385
|
if s3_bucket.notification_configuration:
|
|
386
386
|
if not s3_notif_ctx:
|
|
387
|
-
s3_notif_ctx = S3EventNotificationContext.
|
|
387
|
+
s3_notif_ctx = S3EventNotificationContext.from_request_context(
|
|
388
388
|
context,
|
|
389
389
|
s3_bucket=s3_bucket,
|
|
390
390
|
s3_object=s3_object,
|
|
@@ -1271,7 +1271,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
1271
1271
|
delete_marker_id = generate_version_id(s3_bucket.versioning_status)
|
|
1272
1272
|
delete_marker = S3DeleteMarker(key=key, version_id=delete_marker_id)
|
|
1273
1273
|
s3_bucket.objects.set(key, delete_marker)
|
|
1274
|
-
s3_notif_ctx = S3EventNotificationContext.
|
|
1274
|
+
s3_notif_ctx = S3EventNotificationContext.from_request_context(
|
|
1275
1275
|
context,
|
|
1276
1276
|
s3_bucket=s3_bucket,
|
|
1277
1277
|
s3_object=delete_marker,
|
|
@@ -1374,7 +1374,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
1374
1374
|
delete_marker_id = generate_version_id(s3_bucket.versioning_status)
|
|
1375
1375
|
delete_marker = S3DeleteMarker(key=object_key, version_id=delete_marker_id)
|
|
1376
1376
|
s3_bucket.objects.set(object_key, delete_marker)
|
|
1377
|
-
s3_notif_ctx = S3EventNotificationContext.
|
|
1377
|
+
s3_notif_ctx = S3EventNotificationContext.from_request_context(
|
|
1378
1378
|
context,
|
|
1379
1379
|
s3_bucket=s3_bucket,
|
|
1380
1380
|
s3_object=delete_marker,
|
|
@@ -2202,7 +2202,7 @@ class S3Provider(S3Api, ServiceLifecycleHook):
|
|
|
2202
2202
|
# TODO: add a way to transition from ongoing-request=true to false? for now it is instant
|
|
2203
2203
|
s3_object.restore = f'ongoing-request="false", expiry-date="{restore_expiration_date}"'
|
|
2204
2204
|
|
|
2205
|
-
s3_notif_ctx_initiated = S3EventNotificationContext.
|
|
2205
|
+
s3_notif_ctx_initiated = S3EventNotificationContext.from_request_context(
|
|
2206
2206
|
context,
|
|
2207
2207
|
s3_bucket=s3_bucket,
|
|
2208
2208
|
s3_object=s3_object,
|
localstack/services/s3/utils.py
CHANGED
|
@@ -7,6 +7,7 @@ import logging
|
|
|
7
7
|
import re
|
|
8
8
|
import time
|
|
9
9
|
import zlib
|
|
10
|
+
from collections.abc import Mapping
|
|
10
11
|
from enum import StrEnum
|
|
11
12
|
from secrets import token_bytes
|
|
12
13
|
from typing import Any, Literal, NamedTuple, Protocol
|
|
@@ -63,7 +64,6 @@ from localstack.services.s3.constants import (
|
|
|
63
64
|
AUTHENTICATED_USERS_ACL_GRANTEE,
|
|
64
65
|
CHECKSUM_ALGORITHMS,
|
|
65
66
|
LOG_DELIVERY_ACL_GRANTEE,
|
|
66
|
-
S3_VIRTUAL_HOST_FORWARDED_HEADER,
|
|
67
67
|
SIGNATURE_V2_PARAMS,
|
|
68
68
|
SIGNATURE_V4_PARAMS,
|
|
69
69
|
SYSTEM_METADATA_SETTABLE_HEADERS,
|
|
@@ -522,7 +522,7 @@ def is_valid_canonical_id(canonical_id: str) -> bool:
|
|
|
522
522
|
return False
|
|
523
523
|
|
|
524
524
|
|
|
525
|
-
def uses_host_addressing(headers:
|
|
525
|
+
def uses_host_addressing(headers: Mapping[str, str]) -> str | None:
|
|
526
526
|
"""
|
|
527
527
|
Determines if the request is targeting S3 with virtual host addressing
|
|
528
528
|
:param headers: the request headers
|
|
@@ -551,15 +551,6 @@ def get_system_metadata_from_request(request: dict) -> Metadata:
|
|
|
551
551
|
return metadata
|
|
552
552
|
|
|
553
553
|
|
|
554
|
-
def forwarded_from_virtual_host_addressed_request(headers: dict[str, str]) -> bool:
|
|
555
|
-
"""
|
|
556
|
-
Determines if the request was forwarded from a v-host addressing style into a path one
|
|
557
|
-
"""
|
|
558
|
-
# we can assume that the host header we are receiving here is actually the header we originally received
|
|
559
|
-
# from the client (because the edge service is forwarding the request in memory)
|
|
560
|
-
return S3_VIRTUAL_HOST_FORWARDED_HEADER in headers
|
|
561
|
-
|
|
562
|
-
|
|
563
554
|
def extract_bucket_name_and_key_from_headers_and_path(
|
|
564
555
|
headers: dict[str, str], path: str
|
|
565
556
|
) -> tuple[str | None, str | None]:
|
|
@@ -8,6 +8,7 @@ from botocore.utils import InvalidArnException
|
|
|
8
8
|
|
|
9
9
|
from localstack.aws.api import RequestContext
|
|
10
10
|
from localstack.aws.api.sns import (
|
|
11
|
+
AmazonResourceName,
|
|
11
12
|
ConfirmSubscriptionResponse,
|
|
12
13
|
CreateTopicResponse,
|
|
13
14
|
GetSMSAttributesResponse,
|
|
@@ -17,6 +18,7 @@ from localstack.aws.api.sns import (
|
|
|
17
18
|
ListString,
|
|
18
19
|
ListSubscriptionsByTopicResponse,
|
|
19
20
|
ListSubscriptionsResponse,
|
|
21
|
+
ListTagsForResourceResponse,
|
|
20
22
|
ListTopicsResponse,
|
|
21
23
|
MapStringToString,
|
|
22
24
|
NotFoundException,
|
|
@@ -26,8 +28,11 @@ from localstack.aws.api.sns import (
|
|
|
26
28
|
SubscribeResponse,
|
|
27
29
|
Subscription,
|
|
28
30
|
SubscriptionAttributesMap,
|
|
31
|
+
TagKeyList,
|
|
29
32
|
TagList,
|
|
33
|
+
TagResourceResponse,
|
|
30
34
|
TopicAttributesMap,
|
|
35
|
+
UntagResourceResponse,
|
|
31
36
|
attributeName,
|
|
32
37
|
attributeValue,
|
|
33
38
|
authenticateOnUnsubscribe,
|
|
@@ -102,6 +107,11 @@ class SnsProvider(SnsApi):
|
|
|
102
107
|
if not attrs.get(k) or not attrs.get(k) == v:
|
|
103
108
|
# TODO:
|
|
104
109
|
raise InvalidParameterException("Fix this Exception message and type")
|
|
110
|
+
tag_resource_success = _check_matching_tags(topic_arn, tags, store)
|
|
111
|
+
if not tag_resource_success:
|
|
112
|
+
raise InvalidParameterException(
|
|
113
|
+
"Invalid parameter: Tags Reason: Topic already exists with different tags"
|
|
114
|
+
)
|
|
105
115
|
return CreateTopicResponse(TopicArn=topic_arn)
|
|
106
116
|
|
|
107
117
|
attributes = attributes or {}
|
|
@@ -121,7 +131,8 @@ class SnsProvider(SnsApi):
|
|
|
121
131
|
raise InvalidParameterException("Invalid parameter: Topic Name")
|
|
122
132
|
|
|
123
133
|
topic = _create_topic(name=name, attributes=attributes, context=context)
|
|
124
|
-
|
|
134
|
+
if tags:
|
|
135
|
+
self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)
|
|
125
136
|
|
|
126
137
|
store.topics[topic_arn] = topic
|
|
127
138
|
|
|
@@ -546,6 +557,34 @@ class SnsProvider(SnsApi):
|
|
|
546
557
|
|
|
547
558
|
return GetSMSAttributesResponse(attributes=return_attributes)
|
|
548
559
|
|
|
560
|
+
def list_tags_for_resource(
|
|
561
|
+
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
|
|
562
|
+
) -> ListTagsForResourceResponse:
|
|
563
|
+
store = sns_stores[context.account_id][context.region]
|
|
564
|
+
tags = store.TAGS.list_tags_for_resource(resource_arn)
|
|
565
|
+
return ListTagsForResourceResponse(Tags=tags.get("Tags"))
|
|
566
|
+
|
|
567
|
+
def tag_resource(
|
|
568
|
+
self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList, **kwargs
|
|
569
|
+
) -> TagResourceResponse:
|
|
570
|
+
unique_tag_keys = {tag["Key"] for tag in tags}
|
|
571
|
+
if len(unique_tag_keys) < len(tags):
|
|
572
|
+
raise InvalidParameterException("Invalid parameter: Duplicated keys are not allowed.")
|
|
573
|
+
store = sns_stores[context.account_id][context.region]
|
|
574
|
+
store.TAGS.tag_resource(resource_arn, tags)
|
|
575
|
+
return TagResourceResponse()
|
|
576
|
+
|
|
577
|
+
def untag_resource(
|
|
578
|
+
self,
|
|
579
|
+
context: RequestContext,
|
|
580
|
+
resource_arn: AmazonResourceName,
|
|
581
|
+
tag_keys: TagKeyList,
|
|
582
|
+
**kwargs,
|
|
583
|
+
) -> UntagResourceResponse:
|
|
584
|
+
store = sns_stores[context.account_id][context.region]
|
|
585
|
+
store.TAGS.untag_resource(resource_arn, tag_keys)
|
|
586
|
+
return UntagResourceResponse()
|
|
587
|
+
|
|
549
588
|
@staticmethod
|
|
550
589
|
def get_store(account_id: str, region: str) -> SnsStore:
|
|
551
590
|
return sns_stores[account_id][region]
|
|
@@ -649,3 +688,24 @@ def _validate_sms_attributes(attributes: dict) -> None:
|
|
|
649
688
|
def _set_sms_attribute_default(store: SnsStore) -> None:
|
|
650
689
|
# TODO: don't call this on every sms attribute crud api call
|
|
651
690
|
store.sms_attributes.setdefault("MonthlySpendLimit", "1")
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _check_matching_tags(topic_arn: str, tags: TagList | None, store: SnsStore) -> bool:
|
|
694
|
+
"""
|
|
695
|
+
Checks if a topic to be created doesn't already exist with different tags
|
|
696
|
+
:param topic_arn: Arn of the topic
|
|
697
|
+
:param tags: Tags to be checked
|
|
698
|
+
:param store: Store object that holds the topics and tags
|
|
699
|
+
:return: False if there is a mismatch in tags, True otherwise
|
|
700
|
+
"""
|
|
701
|
+
existing_tags = store.TAGS.list_tags_for_resource(topic_arn)["Tags"]
|
|
702
|
+
# if this is none there is nothing to check
|
|
703
|
+
if topic_arn in store.topics:
|
|
704
|
+
if tags is None:
|
|
705
|
+
tags = []
|
|
706
|
+
for tag in tags:
|
|
707
|
+
# this means topic already created with empty tags and when we try to create it
|
|
708
|
+
# again with other tag value then it should fail according to aws documentation.
|
|
709
|
+
if existing_tags is not None and tag not in existing_tags:
|
|
710
|
+
return False
|
|
711
|
+
return True
|
|
@@ -29,7 +29,7 @@ def load_template_file(file_path: str | os.PathLike, *, path_ctx: str | os.PathL
|
|
|
29
29
|
elif not file_path_obj.is_absolute():
|
|
30
30
|
raise ValueError("Provided path must be absolute if no path_ctx is provided")
|
|
31
31
|
|
|
32
|
-
return load_file(file_path_obj.absolute())
|
|
32
|
+
return load_file(file_path_obj.absolute(), strict=True)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
# TODO: TBH this utility really doesn't add anything, probably better to just remove it
|
|
@@ -63,8 +63,6 @@ def pytest_runtestloop(session: Session):
|
|
|
63
63
|
return
|
|
64
64
|
LOG.info("TEST_FORCE_LOCALSTACK_START is set, a Localstack instance will be created.")
|
|
65
65
|
|
|
66
|
-
from localstack.utils.common import safe_requests
|
|
67
|
-
|
|
68
66
|
if is_aws_cloud():
|
|
69
67
|
localstack_config.DEFAULT_DELAY = 5
|
|
70
68
|
localstack_config.DEFAULT_MAX_ATTEMPTS = 60
|
|
@@ -73,8 +71,6 @@ def pytest_runtestloop(session: Session):
|
|
|
73
71
|
os.environ[ENV_INTERNAL_TEST_RUN] = "1"
|
|
74
72
|
localstack_config.INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = True
|
|
75
73
|
|
|
76
|
-
safe_requests.verify_ssl = False
|
|
77
|
-
|
|
78
74
|
from localstack.runtime import current
|
|
79
75
|
|
|
80
76
|
_started.set()
|
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:
|
|
@@ -288,7 +305,7 @@ def cleanup_tmp_files():
|
|
|
288
305
|
del TMP_FILES[:]
|
|
289
306
|
|
|
290
307
|
|
|
291
|
-
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:
|
|
292
309
|
"""Return a path to a new temporary file."""
|
|
293
310
|
tmp_file, tmp_path = tempfile.mkstemp(suffix=suffix, dir=dir)
|
|
294
311
|
os.close(tmp_file)
|
|
@@ -296,8 +313,15 @@ def new_tmp_file(suffix: str = None, dir: str = None) -> str:
|
|
|
296
313
|
return tmp_path
|
|
297
314
|
|
|
298
315
|
|
|
299
|
-
def new_tmp_dir(dir: str = None):
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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)
|
|
303
327
|
return folder
|
localstack/utils/json.py
CHANGED
|
@@ -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/version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '4.9.3.
|
|
32
|
-
__version_tuple__ = version_tuple = (4, 9, 3, '
|
|
31
|
+
__version__ = version = '4.9.3.dev57'
|
|
32
|
+
__version_tuple__ = version_tuple = (4, 9, 3, 'dev57')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localstack-core
|
|
3
|
-
Version: 4.9.3.
|
|
3
|
+
Version: 4.9.3.dev57
|
|
4
4
|
Summary: The core library and runtime of LocalStack
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -15,7 +15,6 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
15
15
|
Classifier: Topic :: System :: Emulators
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
License-File: LICENSE.txt
|
|
18
|
-
Requires-Dist: build
|
|
19
18
|
Requires-Dist: click>=7.1
|
|
20
19
|
Requires-Dist: cachetools>=5.0
|
|
21
20
|
Requires-Dist: cryptography
|
|
@@ -29,48 +28,53 @@ Requires-Dist: pyyaml>=5.1
|
|
|
29
28
|
Requires-Dist: rich>=12.3.0
|
|
30
29
|
Requires-Dist: requests>=2.20.0
|
|
31
30
|
Requires-Dist: semver>=2.10
|
|
32
|
-
Requires-Dist: tailer>=0.4.1
|
|
33
31
|
Provides-Extra: base-runtime
|
|
34
|
-
Requires-Dist: boto3==1.40.
|
|
35
|
-
Requires-Dist: botocore==1.40.
|
|
32
|
+
Requires-Dist: boto3==1.40.55; extra == "base-runtime"
|
|
33
|
+
Requires-Dist: botocore==1.40.55; extra == "base-runtime"
|
|
36
34
|
Requires-Dist: awscrt!=0.27.1,>=0.13.14; extra == "base-runtime"
|
|
37
35
|
Requires-Dist: cbor2>=5.5.0; extra == "base-runtime"
|
|
38
36
|
Requires-Dist: dnspython>=1.16.0; extra == "base-runtime"
|
|
39
37
|
Requires-Dist: docker>=6.1.1; extra == "base-runtime"
|
|
40
38
|
Requires-Dist: jsonpatch>=1.24; extra == "base-runtime"
|
|
39
|
+
Requires-Dist: jsonpointer>=3.0.0; extra == "base-runtime"
|
|
40
|
+
Requires-Dist: jsonschema>=4.25.1; extra == "base-runtime"
|
|
41
41
|
Requires-Dist: hypercorn>=0.14.4; extra == "base-runtime"
|
|
42
42
|
Requires-Dist: localstack-twisted>=23.0; extra == "base-runtime"
|
|
43
43
|
Requires-Dist: openapi-core>=0.19.2; extra == "base-runtime"
|
|
44
44
|
Requires-Dist: pyopenssl>=23.0.0; extra == "base-runtime"
|
|
45
|
+
Requires-Dist: python-dateutil>=2.9.0; extra == "base-runtime"
|
|
45
46
|
Requires-Dist: readerwriterlock>=1.0.7; extra == "base-runtime"
|
|
46
47
|
Requires-Dist: requests-aws4auth>=1.0; extra == "base-runtime"
|
|
48
|
+
Requires-Dist: typing-extensions>=4.15.0; extra == "base-runtime"
|
|
47
49
|
Requires-Dist: urllib3>=2.0.7; extra == "base-runtime"
|
|
48
50
|
Requires-Dist: Werkzeug>=3.1.3; extra == "base-runtime"
|
|
49
51
|
Requires-Dist: xmltodict>=0.13.0; extra == "base-runtime"
|
|
50
52
|
Requires-Dist: rolo>=0.7; extra == "base-runtime"
|
|
51
53
|
Provides-Extra: runtime
|
|
52
54
|
Requires-Dist: localstack-core[base-runtime]; extra == "runtime"
|
|
53
|
-
Requires-Dist: awscli==1.42.
|
|
55
|
+
Requires-Dist: awscli==1.42.55; extra == "runtime"
|
|
54
56
|
Requires-Dist: airspeed-ext>=0.6.3; extra == "runtime"
|
|
55
|
-
Requires-Dist: kclpy-ext>=3.0.0; extra == "runtime"
|
|
56
57
|
Requires-Dist: antlr4-python3-runtime==4.13.2; extra == "runtime"
|
|
57
58
|
Requires-Dist: apispec>=5.1.1; extra == "runtime"
|
|
58
59
|
Requires-Dist: aws-sam-translator>=1.15.1; extra == "runtime"
|
|
59
60
|
Requires-Dist: crontab>=0.22.6; extra == "runtime"
|
|
60
61
|
Requires-Dist: cryptography>=41.0.5; extra == "runtime"
|
|
62
|
+
Requires-Dist: jinja2>=3.1.6; extra == "runtime"
|
|
61
63
|
Requires-Dist: jpype1>=1.6.0; extra == "runtime"
|
|
62
|
-
Requires-Dist: json5>=0.9.11; extra == "runtime"
|
|
63
64
|
Requires-Dist: jsonpath-ng>=1.6.1; extra == "runtime"
|
|
64
65
|
Requires-Dist: jsonpath-rw>=1.4.0; extra == "runtime"
|
|
66
|
+
Requires-Dist: kclpy-ext>=3.0.0; extra == "runtime"
|
|
65
67
|
Requires-Dist: moto-ext[all]>=5.1.12.post22; extra == "runtime"
|
|
66
68
|
Requires-Dist: opensearch-py>=2.4.1; extra == "runtime"
|
|
69
|
+
Requires-Dist: pydantic>=2.11.9; extra == "runtime"
|
|
67
70
|
Requires-Dist: pymongo>=4.2.0; extra == "runtime"
|
|
68
71
|
Requires-Dist: pyopenssl>=23.0.0; extra == "runtime"
|
|
72
|
+
Requires-Dist: responses>=0.25.8; extra == "runtime"
|
|
69
73
|
Provides-Extra: test
|
|
70
74
|
Requires-Dist: localstack-core[runtime]; extra == "test"
|
|
71
75
|
Requires-Dist: coverage[toml]>=5.5; extra == "test"
|
|
72
|
-
Requires-Dist: deepdiff>=6.4.1; extra == "test"
|
|
73
76
|
Requires-Dist: httpx[http2]>=0.25; extra == "test"
|
|
77
|
+
Requires-Dist: json5>=0.12.1; extra == "test"
|
|
74
78
|
Requires-Dist: pluggy>=1.3.0; extra == "test"
|
|
75
79
|
Requires-Dist: pytest>=7.4.2; extra == "test"
|
|
76
80
|
Requires-Dist: pytest-split>=0.8.0; extra == "test"
|
|
@@ -83,6 +87,7 @@ Requires-Dist: localstack-snapshot>=0.1.1; extra == "test"
|
|
|
83
87
|
Provides-Extra: dev
|
|
84
88
|
Requires-Dist: localstack-core[test]; extra == "dev"
|
|
85
89
|
Requires-Dist: coveralls>=3.3.1; extra == "dev"
|
|
90
|
+
Requires-Dist: deptry>=0.13.0; extra == "dev"
|
|
86
91
|
Requires-Dist: Cython; extra == "dev"
|
|
87
92
|
Requires-Dist: networkx>=2.8.4; extra == "dev"
|
|
88
93
|
Requires-Dist: openapi-spec-validator>=0.7.1; extra == "dev"
|