playground-ls-cli 4.14.1.dev8__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_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package provides custom collection types, as well as tools to analyze
|
|
3
|
+
and manipulate python collection (dicts, list, sets).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sized
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Optional,
|
|
12
|
+
TypedDict,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
cast,
|
|
16
|
+
get_args,
|
|
17
|
+
get_origin,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
import cachetools
|
|
21
|
+
|
|
22
|
+
LOG = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# default regex to match an item in a comma-separated list string
|
|
25
|
+
DEFAULT_REGEX_LIST_ITEM = r"[\w-]+"
|
|
26
|
+
|
|
27
|
+
_E = TypeVar("_E")
|
|
28
|
+
"""TypeVar var used internally for container type parameters."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AccessTrackingDict(dict):
|
|
32
|
+
"""
|
|
33
|
+
Simple utility class that can be used to track (write) accesses to a dict's attributes.
|
|
34
|
+
Note: could also be written as a proxy, to preserve the identity of "wrapped" - for now, it
|
|
35
|
+
simply duplicates the entries of "wrapped" in the constructor, for simplicity.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, wrapped, callback: Callable[[dict, str, list, dict], Any] = None):
|
|
39
|
+
super().__init__(wrapped)
|
|
40
|
+
self.callback = callback
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, key, value):
|
|
43
|
+
self.callback and self.callback(self, "__setitem__", [key, value], {})
|
|
44
|
+
return super().__setitem__(key, value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DelSafeDict(dict):
|
|
48
|
+
"""Useful when applying jsonpatch. Use it as follows:
|
|
49
|
+
|
|
50
|
+
obj.__dict__ = DelSafeDict(obj.__dict__)
|
|
51
|
+
apply_patch(obj.__dict__, patch)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __delitem__(self, key, *args, **kwargs):
|
|
55
|
+
self[key] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ImmutableList(tuple):
|
|
59
|
+
"""
|
|
60
|
+
Wrapper class to create an immutable view of a given list or sequence.
|
|
61
|
+
Note: Currently, this is simply a wrapper around `tuple` - could be replaced with
|
|
62
|
+
custom implementations over time, if needed.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HashableList(ImmutableList):
|
|
67
|
+
"""Hashable, immutable list wrapper that can be used with dicts or hash sets."""
|
|
68
|
+
|
|
69
|
+
def __hash__(self):
|
|
70
|
+
return sum(hash(i) for i in self)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ImmutableDict(Mapping):
|
|
74
|
+
"""Wrapper class to create an immutable view of a given list or sequence."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, seq=None, **kwargs):
|
|
77
|
+
self._dict = dict(seq, **kwargs)
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
return self._dict.__len__()
|
|
81
|
+
|
|
82
|
+
def __iter__(self) -> Iterator:
|
|
83
|
+
return self._dict.__iter__()
|
|
84
|
+
|
|
85
|
+
def __getitem__(self, key):
|
|
86
|
+
return self._dict.__getitem__(key)
|
|
87
|
+
|
|
88
|
+
def __eq__(self, other):
|
|
89
|
+
return self._dict.__eq__(other._dict if isinstance(other, ImmutableDict) else other)
|
|
90
|
+
|
|
91
|
+
def __str__(self):
|
|
92
|
+
return self._dict.__str__()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class HashableJsonDict(ImmutableDict):
|
|
96
|
+
"""
|
|
97
|
+
Simple dict wrapper that can be used with dicts or hash sets. Note: the assumption is that the dict
|
|
98
|
+
can be JSON-encoded (i.e., must be acyclic and contain only lists/dicts and simple types)
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __hash__(self):
|
|
102
|
+
from localstack_cli.utils.json import canonical_json
|
|
103
|
+
|
|
104
|
+
return hash(canonical_json(self._dict))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PaginatedList(list[_E]):
|
|
108
|
+
"""List which can be paginated and filtered. For usage in AWS APIs with paginated responses"""
|
|
109
|
+
|
|
110
|
+
DEFAULT_PAGE_SIZE = 50
|
|
111
|
+
|
|
112
|
+
def get_page(
|
|
113
|
+
self,
|
|
114
|
+
token_generator: Callable[[_E], str],
|
|
115
|
+
next_token: str = None,
|
|
116
|
+
page_size: int = None,
|
|
117
|
+
filter_function: Callable[[_E], bool] = None,
|
|
118
|
+
) -> tuple[list[_E], str | None]:
|
|
119
|
+
if filter_function is not None:
|
|
120
|
+
result_list = list(filter(filter_function, self))
|
|
121
|
+
else:
|
|
122
|
+
result_list = self
|
|
123
|
+
|
|
124
|
+
if page_size is None:
|
|
125
|
+
page_size = self.DEFAULT_PAGE_SIZE
|
|
126
|
+
|
|
127
|
+
# returns all or remaining elements in final page.
|
|
128
|
+
if len(result_list) <= page_size and next_token is None:
|
|
129
|
+
return result_list, None
|
|
130
|
+
|
|
131
|
+
start_idx = 0
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
start_item = next(item for item in result_list if token_generator(item) == next_token)
|
|
135
|
+
start_idx = result_list.index(start_item)
|
|
136
|
+
except StopIteration:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
if start_idx + page_size < len(result_list):
|
|
140
|
+
next_token = token_generator(result_list[start_idx + page_size])
|
|
141
|
+
else:
|
|
142
|
+
next_token = None
|
|
143
|
+
|
|
144
|
+
return result_list[start_idx : start_idx + page_size], next_token
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CustomExpiryTTLCache(cachetools.TTLCache):
|
|
148
|
+
"""TTLCache that allows to set custom expiry times for individual keys."""
|
|
149
|
+
|
|
150
|
+
def set_expiry(self, key: Any, ttl: float | int) -> float:
|
|
151
|
+
"""Set the expiry of the given key in a TTLCache to (<current_time> + <ttl>)"""
|
|
152
|
+
with self.timer as time:
|
|
153
|
+
# note: need to access the internal dunder API here
|
|
154
|
+
self._TTLCache__getlink(key).expires = expiry = time + ttl
|
|
155
|
+
return expiry
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_safe(dictionary, path, default_value=None):
|
|
159
|
+
"""
|
|
160
|
+
Performs a safe navigation on a Dictionary object and
|
|
161
|
+
returns the result or default value (if specified).
|
|
162
|
+
The function follows a common AWS path resolution pattern "$.a.b.c".
|
|
163
|
+
|
|
164
|
+
:type dictionary: dict
|
|
165
|
+
:param dictionary: Dict to perform safe navigation.
|
|
166
|
+
|
|
167
|
+
:type path: list|str
|
|
168
|
+
:param path: List or dot-separated string containing the path of an attribute,
|
|
169
|
+
starting from the root node "$".
|
|
170
|
+
|
|
171
|
+
:type default_value: any
|
|
172
|
+
:param default_value: Default value to return in case resolved value is None.
|
|
173
|
+
|
|
174
|
+
:rtype: any
|
|
175
|
+
:return: Resolved value or default_value.
|
|
176
|
+
"""
|
|
177
|
+
if not isinstance(dictionary, dict) or len(dictionary) == 0:
|
|
178
|
+
return default_value
|
|
179
|
+
|
|
180
|
+
attribute_path = path if isinstance(path, list) else path.split(".")
|
|
181
|
+
if len(attribute_path) == 0 or attribute_path[0] != "$":
|
|
182
|
+
raise AttributeError('Safe navigation must begin with a root node "$"')
|
|
183
|
+
|
|
184
|
+
current_value = dictionary
|
|
185
|
+
for path_node in attribute_path:
|
|
186
|
+
if path_node == "$":
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
if re.compile("^\\d+$").search(str(path_node)):
|
|
190
|
+
path_node = int(path_node)
|
|
191
|
+
|
|
192
|
+
if isinstance(current_value, dict) and path_node in current_value:
|
|
193
|
+
current_value = current_value[path_node]
|
|
194
|
+
elif isinstance(current_value, list) and path_node < len(current_value):
|
|
195
|
+
current_value = current_value[path_node]
|
|
196
|
+
else:
|
|
197
|
+
current_value = None
|
|
198
|
+
|
|
199
|
+
return current_value or default_value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def set_safe_mutable(dictionary, path, value):
|
|
203
|
+
"""
|
|
204
|
+
Mutates original dict and sets the specified value under provided path.
|
|
205
|
+
|
|
206
|
+
:type dictionary: dict
|
|
207
|
+
:param dictionary: Dict to mutate.
|
|
208
|
+
|
|
209
|
+
:type path: list|str
|
|
210
|
+
:param path: List or dot-separated string containing the path of an attribute,
|
|
211
|
+
starting from the root node "$".
|
|
212
|
+
|
|
213
|
+
:type value: any
|
|
214
|
+
:param value: Value to set under specified path.
|
|
215
|
+
|
|
216
|
+
:rtype: dict
|
|
217
|
+
:return: Returns mutated dictionary.
|
|
218
|
+
"""
|
|
219
|
+
if not isinstance(dictionary, dict):
|
|
220
|
+
raise AttributeError('"dictionary" must be of type "dict"')
|
|
221
|
+
|
|
222
|
+
attribute_path = path if isinstance(path, list) else path.split(".")
|
|
223
|
+
attribute_path_len = len(attribute_path)
|
|
224
|
+
|
|
225
|
+
if attribute_path_len == 0 or attribute_path[0] != "$":
|
|
226
|
+
raise AttributeError('Dict navigation must begin with a root node "$"')
|
|
227
|
+
|
|
228
|
+
current_pointer = dictionary
|
|
229
|
+
for i in range(attribute_path_len):
|
|
230
|
+
path_node = attribute_path[i]
|
|
231
|
+
|
|
232
|
+
if path_node == "$":
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if i < attribute_path_len - 1:
|
|
236
|
+
if path_node not in current_pointer:
|
|
237
|
+
current_pointer[path_node] = {}
|
|
238
|
+
if not isinstance(current_pointer, dict):
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
'Error while deeply setting a dict value. Supplied path is not of type "dict"'
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
current_pointer[path_node] = value
|
|
244
|
+
|
|
245
|
+
current_pointer = current_pointer[path_node]
|
|
246
|
+
|
|
247
|
+
return dictionary
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def pick_attributes(dictionary, paths):
|
|
251
|
+
"""
|
|
252
|
+
Picks selected attributes a returns them as a new dictionary.
|
|
253
|
+
This function works as a whitelist of attributes to keep in a new dictionary.
|
|
254
|
+
|
|
255
|
+
:type dictionary: dict
|
|
256
|
+
:param dictionary: Dict to pick attributes from.
|
|
257
|
+
|
|
258
|
+
:type paths: list of (list or str)
|
|
259
|
+
:param paths: List of lists or strings with dot-separated paths, starting from the root node "$".
|
|
260
|
+
|
|
261
|
+
:rtype: dict
|
|
262
|
+
:return: Returns whitelisted dictionary.
|
|
263
|
+
"""
|
|
264
|
+
new_dictionary = {}
|
|
265
|
+
|
|
266
|
+
for path in paths:
|
|
267
|
+
value = get_safe(dictionary, path)
|
|
268
|
+
|
|
269
|
+
if value is not None:
|
|
270
|
+
set_safe_mutable(new_dictionary, path, value)
|
|
271
|
+
|
|
272
|
+
return new_dictionary
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def select_attributes(obj: dict, attributes: list[str]) -> dict:
|
|
276
|
+
"""Select a subset of attributes from the given dict (returns a copy)"""
|
|
277
|
+
attributes = attributes if is_list_or_tuple(attributes) else [attributes]
|
|
278
|
+
return {k: v for k, v in obj.items() if k in attributes}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def remove_attributes(obj: dict, attributes: list[str], recursive: bool = False) -> dict:
|
|
282
|
+
"""Remove a set of attributes from the given dict (in-place)"""
|
|
283
|
+
from localstack_cli.utils.objects import recurse_object
|
|
284
|
+
|
|
285
|
+
if recursive:
|
|
286
|
+
|
|
287
|
+
def _remove(o, **kwargs):
|
|
288
|
+
if isinstance(o, dict):
|
|
289
|
+
remove_attributes(o, attributes)
|
|
290
|
+
return o
|
|
291
|
+
|
|
292
|
+
return recurse_object(obj, _remove)
|
|
293
|
+
|
|
294
|
+
attributes = ensure_list(attributes)
|
|
295
|
+
for attr in attributes:
|
|
296
|
+
obj.pop(attr, None)
|
|
297
|
+
return obj
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def rename_attributes(
|
|
301
|
+
obj: dict, old_to_new_attributes: dict[str, str], in_place: bool = False
|
|
302
|
+
) -> dict:
|
|
303
|
+
"""Rename a set of attributes in the given dict object. Second parameter is a dict that maps old to
|
|
304
|
+
new attribute names. Default is to return a copy, but can also pass in_place=True."""
|
|
305
|
+
if not in_place:
|
|
306
|
+
obj = dict(obj)
|
|
307
|
+
for old_name, new_name in old_to_new_attributes.items():
|
|
308
|
+
if old_name in obj:
|
|
309
|
+
obj[new_name] = obj.pop(old_name)
|
|
310
|
+
return obj
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def is_list_or_tuple(obj) -> bool:
|
|
314
|
+
return isinstance(obj, (list, tuple))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def ensure_list(obj: Any, wrap_none=False) -> list | None:
|
|
318
|
+
"""Wrap the given object in a list, or return the object itself if it already is a list."""
|
|
319
|
+
if obj is None and not wrap_none:
|
|
320
|
+
return obj
|
|
321
|
+
return obj if isinstance(obj, list) else [obj]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def to_unique_items_list(inputs, comparator=None):
|
|
325
|
+
"""Return a list of unique items from the given input iterable.
|
|
326
|
+
The comparator(item1, item2) returns True/False or an int for comparison."""
|
|
327
|
+
|
|
328
|
+
def contained(item):
|
|
329
|
+
for r in result:
|
|
330
|
+
if comparator:
|
|
331
|
+
cmp_res = comparator(item, r)
|
|
332
|
+
if cmp_res is True or str(cmp_res) == "0":
|
|
333
|
+
return True
|
|
334
|
+
elif item == r:
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
result = []
|
|
338
|
+
for it in inputs:
|
|
339
|
+
if not contained(it):
|
|
340
|
+
result.append(it)
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def merge_recursive(source, destination, none_values=None, overwrite=False):
|
|
345
|
+
if none_values is None:
|
|
346
|
+
none_values = [None]
|
|
347
|
+
for key, value in source.items():
|
|
348
|
+
if isinstance(value, dict):
|
|
349
|
+
# get node or create one
|
|
350
|
+
node = destination.setdefault(key, {})
|
|
351
|
+
merge_recursive(value, node, none_values=none_values, overwrite=overwrite)
|
|
352
|
+
else:
|
|
353
|
+
from requests.models import CaseInsensitiveDict
|
|
354
|
+
|
|
355
|
+
if not isinstance(destination, (dict, CaseInsensitiveDict)):
|
|
356
|
+
LOG.warning(
|
|
357
|
+
"Destination for merging %s=%s is not dict: %s (%s)",
|
|
358
|
+
key,
|
|
359
|
+
value,
|
|
360
|
+
destination,
|
|
361
|
+
type(destination),
|
|
362
|
+
)
|
|
363
|
+
if overwrite or destination.get(key) in none_values:
|
|
364
|
+
destination[key] = value
|
|
365
|
+
return destination
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def merge_dicts(*dicts, **kwargs):
|
|
369
|
+
"""Merge all dicts in `*dicts` into a single dict, and return the result. If any of the entries
|
|
370
|
+
in `*dicts` is None, and `default` is specified as keyword argument, then return `default`."""
|
|
371
|
+
result = {}
|
|
372
|
+
for d in dicts:
|
|
373
|
+
if d is None and "default" in kwargs:
|
|
374
|
+
return kwargs["default"]
|
|
375
|
+
if d:
|
|
376
|
+
result.update(d)
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def remove_none_values_from_dict(dict: dict) -> dict:
|
|
381
|
+
return {k: v for (k, v) in dict.items() if v is not None}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def last_index_of(array, value):
|
|
385
|
+
"""Return the last index of `value` in the given list, or -1 if it does not exist."""
|
|
386
|
+
result = -1
|
|
387
|
+
for i in reversed(range(len(array))):
|
|
388
|
+
entry = array[i]
|
|
389
|
+
if entry == value or (callable(value) and value(entry)):
|
|
390
|
+
return i
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def is_sub_dict(child_dict: dict, parent_dict: dict) -> bool:
|
|
395
|
+
"""Returns whether the first dict is a sub-dict (subset) of the second dict."""
|
|
396
|
+
return all(parent_dict.get(key) == val for key, val in child_dict.items())
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def items_equivalent(list1, list2, comparator):
|
|
400
|
+
"""Returns whether two lists are equivalent (i.e., same items contained in both lists,
|
|
401
|
+
irrespective of the items' order) with respect to a comparator function."""
|
|
402
|
+
|
|
403
|
+
def contained(item):
|
|
404
|
+
for _item in list2:
|
|
405
|
+
if comparator(item, _item):
|
|
406
|
+
return True
|
|
407
|
+
|
|
408
|
+
if len(list1) != len(list2):
|
|
409
|
+
return False
|
|
410
|
+
for item in list1:
|
|
411
|
+
if not contained(item):
|
|
412
|
+
return False
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def is_none_or_empty(obj: str | None | list | None) -> bool:
|
|
417
|
+
return (
|
|
418
|
+
obj is None
|
|
419
|
+
or (isinstance(obj, str) and obj.strip() == "")
|
|
420
|
+
or (isinstance(obj, Sized) and len(obj) == 0)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def select_from_typed_dict(typed_dict: type[TypedDict], obj: dict, filter: bool = False) -> dict:
|
|
425
|
+
"""
|
|
426
|
+
Select a subset of attributes from a dictionary based on the keys of a given `TypedDict`.
|
|
427
|
+
:param typed_dict: the `TypedDict` blueprint
|
|
428
|
+
:param obj: the object to filter
|
|
429
|
+
:param filter: if True, remove all keys with an empty (e.g., empty string or dictionary) or `None` value
|
|
430
|
+
:return: the resulting dictionary (it returns a copy)
|
|
431
|
+
"""
|
|
432
|
+
selection = select_attributes(
|
|
433
|
+
obj, [*typed_dict.__required_keys__, *typed_dict.__optional_keys__]
|
|
434
|
+
)
|
|
435
|
+
if filter:
|
|
436
|
+
selection = {k: v for k, v in selection.items() if v}
|
|
437
|
+
return selection
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
T = TypeVar("T", bound=dict)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def convert_to_typed_dict(typed_dict: type[T], obj: dict, strict: bool = False) -> T:
|
|
444
|
+
"""
|
|
445
|
+
Converts the given object to the given typed dict (by calling the type constructors).
|
|
446
|
+
Limitations:
|
|
447
|
+
- This does not work for ForwardRefs (type refs in quotes).
|
|
448
|
+
- If a type is a Union, the first type is used for the conversion.
|
|
449
|
+
- The conversion fails for types which cannot be instantiated with the constructor.
|
|
450
|
+
|
|
451
|
+
:param typed_dict: to convert the given object to
|
|
452
|
+
:param obj: object to convert matching keys to the types defined in the typed dict
|
|
453
|
+
:param strict: True if a TypeError should be raised in case the conversion fails
|
|
454
|
+
:return: obj converted to the typed dict T
|
|
455
|
+
"""
|
|
456
|
+
result = cast(T, select_from_typed_dict(typed_dict, obj, filter=True))
|
|
457
|
+
for key, key_type in typed_dict.__annotations__.items():
|
|
458
|
+
if key in result:
|
|
459
|
+
# If it's a Union, or optional, we extract the first type argument
|
|
460
|
+
if get_origin(key_type) in [Union, Optional]:
|
|
461
|
+
key_type = get_args(key_type)[0]
|
|
462
|
+
# Use duck-typing to check if the dict is a typed dict
|
|
463
|
+
if hasattr(key_type, "__required_keys__") and hasattr(key_type, "__optional_keys__"):
|
|
464
|
+
result[key] = convert_to_typed_dict(key_type, result[key])
|
|
465
|
+
else:
|
|
466
|
+
# Otherwise, we call the type's constructor (on a best-effort basis)
|
|
467
|
+
try:
|
|
468
|
+
result[key] = key_type(result[key])
|
|
469
|
+
except TypeError as e:
|
|
470
|
+
if strict:
|
|
471
|
+
raise e
|
|
472
|
+
else:
|
|
473
|
+
LOG.debug("Could not convert %s to %s.", key, key_type)
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def dict_multi_values(elements: list | dict) -> dict[str, list[Any]]:
|
|
478
|
+
"""
|
|
479
|
+
Return a dictionary with the original keys from the list of dictionary and the
|
|
480
|
+
values are the list of values of the original dictionary.
|
|
481
|
+
"""
|
|
482
|
+
result_dict = {}
|
|
483
|
+
if isinstance(elements, dict):
|
|
484
|
+
for key, value in elements.items():
|
|
485
|
+
if isinstance(value, list):
|
|
486
|
+
result_dict[key] = value
|
|
487
|
+
else:
|
|
488
|
+
result_dict[key] = [value]
|
|
489
|
+
elif isinstance(elements, list):
|
|
490
|
+
if isinstance(elements[0], list):
|
|
491
|
+
for key, value in elements:
|
|
492
|
+
if key in result_dict:
|
|
493
|
+
result_dict[key].append(value)
|
|
494
|
+
else:
|
|
495
|
+
result_dict[key] = [value]
|
|
496
|
+
else:
|
|
497
|
+
result_dict[elements[0]] = elements[1:]
|
|
498
|
+
return result_dict
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
ItemType = TypeVar("ItemType")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def split_list_by(
|
|
505
|
+
lst: Iterable[ItemType], predicate: Callable[[ItemType], bool]
|
|
506
|
+
) -> tuple[list[ItemType], list[ItemType]]:
|
|
507
|
+
truthy, falsy = [], []
|
|
508
|
+
|
|
509
|
+
for item in lst:
|
|
510
|
+
if predicate(item):
|
|
511
|
+
truthy.append(item)
|
|
512
|
+
else:
|
|
513
|
+
falsy.append(item)
|
|
514
|
+
|
|
515
|
+
return truthy, falsy
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def is_comma_delimited_list(string: str, item_regex: str | None = None) -> bool:
|
|
519
|
+
"""
|
|
520
|
+
Checks if the given string is a comma-delimited list of items.
|
|
521
|
+
The optional `item_regex` parameter specifies the regex pattern for each item in the list.
|
|
522
|
+
"""
|
|
523
|
+
item_regex = item_regex or DEFAULT_REGEX_LIST_ITEM
|
|
524
|
+
|
|
525
|
+
pattern = re.compile(rf"^\s*({item_regex})(\s*,\s*{item_regex})*\s*$")
|
|
526
|
+
if pattern.match(string) is None:
|
|
527
|
+
return False
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def optional_list(condition: bool, items: Iterable[_E]) -> list[_E]:
|
|
532
|
+
"""
|
|
533
|
+
Given an iterable, either create a list out of the entire iterable (if `condition` is `True`), or return the empty list.
|
|
534
|
+
>>> print(optional_list(True, [1, 2, 3]))
|
|
535
|
+
[1, 2, 3]
|
|
536
|
+
>>> print(optional_list(False, [1, 2, 3]))
|
|
537
|
+
[]
|
|
538
|
+
"""
|
|
539
|
+
return list(filter(lambda _: condition, items))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def iter_chunks(items: list[_E], chunk_size: int) -> Generator[list[_E], None, None]:
|
|
543
|
+
"""
|
|
544
|
+
Split a list into smaller chunks of a specified size and iterate over them.
|
|
545
|
+
|
|
546
|
+
It is implemented as a generator and yields each chunk as needed, making it memory-efficient for large lists.
|
|
547
|
+
|
|
548
|
+
:param items: A list of elements to be divided into chunks.
|
|
549
|
+
:param chunk_size: The maximum number of elements that a single chunk can contain.
|
|
550
|
+
:return: A generator that yields chunks (sublists) of the original list. Each chunk contains up to `chunk_size`
|
|
551
|
+
elements.
|
|
552
|
+
"""
|
|
553
|
+
for i in range(0, len(items), chunk_size):
|
|
554
|
+
yield items[i : i + chunk_size]
|