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,327 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
LOG = logging.getLogger(__name__)
|
|
11
|
+
TMP_FILES = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_config_file(file_or_str: str, single_section: bool = True) -> dict:
|
|
15
|
+
"""Parse the given properties config file/string and return a dict of section->key->value.
|
|
16
|
+
If the config contains a single section, and 'single_section' is True, returns"""
|
|
17
|
+
|
|
18
|
+
config = configparser.RawConfigParser()
|
|
19
|
+
|
|
20
|
+
if os.path.exists(file_or_str):
|
|
21
|
+
file_or_str = load_file(file_or_str)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
config.read_string(file_or_str)
|
|
25
|
+
except configparser.MissingSectionHeaderError:
|
|
26
|
+
file_or_str = f"[default]\n{file_or_str}"
|
|
27
|
+
config.read_string(file_or_str)
|
|
28
|
+
|
|
29
|
+
sections = list(config.sections())
|
|
30
|
+
|
|
31
|
+
result = {sec: dict(config.items(sec)) for sec in sections}
|
|
32
|
+
if len(sections) == 1 and single_section:
|
|
33
|
+
result = result[sections[0]]
|
|
34
|
+
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_user_cache_dir() -> Path:
|
|
39
|
+
"""
|
|
40
|
+
Returns the path of the user's cache dir (e.g., ~/.cache on Linux, or ~/Library/Caches on Mac).
|
|
41
|
+
|
|
42
|
+
:return: a Path pointing to the platform-specific cache dir of the user
|
|
43
|
+
"""
|
|
44
|
+
from localstack_cli.utils.platform import is_linux, is_mac_os, is_windows
|
|
45
|
+
|
|
46
|
+
if is_windows():
|
|
47
|
+
return Path(os.path.expandvars(r"%LOCALAPPDATA%\cache"))
|
|
48
|
+
if is_mac_os():
|
|
49
|
+
return Path.home() / "Library" / "Caches"
|
|
50
|
+
if is_linux():
|
|
51
|
+
string_path = os.environ.get("XDG_CACHE_HOME")
|
|
52
|
+
if string_path and os.path.isabs(string_path):
|
|
53
|
+
return Path(string_path)
|
|
54
|
+
# Use the common place to store caches in Linux as a default
|
|
55
|
+
return Path.home() / ".cache"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cache_dir() -> Path:
|
|
59
|
+
"""
|
|
60
|
+
Returns the cache dir for localstack (e.g., ~/.cache/localstack)
|
|
61
|
+
|
|
62
|
+
:return: a Path pointing to the localstack cache dir
|
|
63
|
+
"""
|
|
64
|
+
return get_user_cache_dir() / "localstack"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save_file(file, content, append=False, permissions=None):
|
|
68
|
+
mode = "a" if append else "w+"
|
|
69
|
+
if not isinstance(content, str):
|
|
70
|
+
mode = mode + "b"
|
|
71
|
+
|
|
72
|
+
def _opener(path, flags):
|
|
73
|
+
return os.open(path, flags, permissions)
|
|
74
|
+
|
|
75
|
+
# make sure that the parent dir exists
|
|
76
|
+
mkdir(os.path.dirname(file))
|
|
77
|
+
# store file contents
|
|
78
|
+
with open(file, mode, opener=_opener if permissions else None) as f:
|
|
79
|
+
f.write(content)
|
|
80
|
+
f.flush()
|
|
81
|
+
|
|
82
|
+
|
|
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
|
+
"""
|
|
98
|
+
if not os.path.isfile(file_path):
|
|
99
|
+
if strict:
|
|
100
|
+
raise FileNotFoundError(file_path)
|
|
101
|
+
else:
|
|
102
|
+
return default
|
|
103
|
+
if not mode:
|
|
104
|
+
mode = "r"
|
|
105
|
+
with open(file_path, mode) as f:
|
|
106
|
+
result = f.read()
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_or_create_file(file_path, content=None, permissions=None):
|
|
111
|
+
if os.path.exists(file_path):
|
|
112
|
+
return load_file(file_path)
|
|
113
|
+
content = "{}" if content is None else content
|
|
114
|
+
try:
|
|
115
|
+
save_file(file_path, content, permissions=permissions)
|
|
116
|
+
return content
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def replace_in_file(search, replace, file_path):
|
|
122
|
+
"""Replace all occurrences of `search` with `replace` in the given file (overwrites in place!)"""
|
|
123
|
+
content = load_file(file_path) or ""
|
|
124
|
+
content_new = content.replace(search, replace)
|
|
125
|
+
if content != content_new:
|
|
126
|
+
save_file(file_path, content_new)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def mkdir(folder: str):
|
|
130
|
+
if not os.path.exists(folder):
|
|
131
|
+
os.makedirs(folder, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_empty_dir(directory: str, ignore_hidden: bool = False) -> bool:
|
|
135
|
+
"""Return whether the given directory contains any entries (files/folders), including hidden
|
|
136
|
+
entries whose name starts with a dot (.), unless ignore_hidden=True is passed."""
|
|
137
|
+
if not os.path.isdir(directory):
|
|
138
|
+
raise Exception(f"Path is not a directory: {directory}")
|
|
139
|
+
entries = os.listdir(directory)
|
|
140
|
+
if ignore_hidden:
|
|
141
|
+
entries = [e for e in entries if not e.startswith(".")]
|
|
142
|
+
return not bool(entries)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def ensure_readable(file_path: str, default_perms: int = None):
|
|
146
|
+
if default_perms is None:
|
|
147
|
+
default_perms = 0o644
|
|
148
|
+
try:
|
|
149
|
+
with open(file_path, "rb"):
|
|
150
|
+
pass
|
|
151
|
+
except Exception:
|
|
152
|
+
LOG.info("Updating permissions as file is currently not readable: %s", file_path)
|
|
153
|
+
os.chmod(file_path, default_perms)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def chown_r(path: str, user: str):
|
|
157
|
+
"""Recursive chown on the given file/directory path."""
|
|
158
|
+
# keep these imports here for Windows compatibility
|
|
159
|
+
import grp
|
|
160
|
+
import pwd
|
|
161
|
+
|
|
162
|
+
uid = pwd.getpwnam(user).pw_uid
|
|
163
|
+
gid = grp.getgrnam(user).gr_gid
|
|
164
|
+
os.chown(path, uid, gid)
|
|
165
|
+
for root, dirs, files in os.walk(path):
|
|
166
|
+
for dirname in dirs:
|
|
167
|
+
os.chown(os.path.join(root, dirname), uid, gid)
|
|
168
|
+
for filename in files:
|
|
169
|
+
os.chown(os.path.join(root, filename), uid, gid)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def chmod_r(path: str, mode: int):
|
|
173
|
+
"""
|
|
174
|
+
Recursive chmod
|
|
175
|
+
:param path: path to file or directory
|
|
176
|
+
:param mode: permission mask as octal integer value
|
|
177
|
+
"""
|
|
178
|
+
if not os.path.exists(path):
|
|
179
|
+
return
|
|
180
|
+
idempotent_chmod(path, mode)
|
|
181
|
+
for root, dirnames, filenames in os.walk(path):
|
|
182
|
+
for dirname in dirnames:
|
|
183
|
+
idempotent_chmod(os.path.join(root, dirname), mode)
|
|
184
|
+
for filename in filenames:
|
|
185
|
+
idempotent_chmod(os.path.join(root, filename), mode)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def idempotent_chmod(path: str, mode: int):
|
|
189
|
+
"""
|
|
190
|
+
Perform idempotent chmod on the given file path (non-recursively). The function attempts to call `os.chmod`, and
|
|
191
|
+
will catch and only re-raise exceptions (e.g., PermissionError) if the file does not have the given mode already.
|
|
192
|
+
:param path: path to file
|
|
193
|
+
:param mode: permission mask as octal integer value
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
os.chmod(path, mode)
|
|
197
|
+
except Exception:
|
|
198
|
+
try:
|
|
199
|
+
existing_mode = os.stat(path)
|
|
200
|
+
except FileNotFoundError:
|
|
201
|
+
# file deleted in the meantime, or otherwise not accessible (socket)
|
|
202
|
+
return
|
|
203
|
+
if mode in (existing_mode.st_mode, stat.S_IMODE(existing_mode.st_mode)):
|
|
204
|
+
# file already has the desired permissions -> return
|
|
205
|
+
return
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def rm_rf(path: str):
|
|
210
|
+
"""
|
|
211
|
+
Recursively removes a file or directory
|
|
212
|
+
"""
|
|
213
|
+
from localstack_cli.utils.platform import is_debian
|
|
214
|
+
from localstack_cli.utils.run import run
|
|
215
|
+
|
|
216
|
+
if not path or not os.path.exists(path):
|
|
217
|
+
return
|
|
218
|
+
# Running the native command can be an order of magnitude faster in Alpine on Travis-CI
|
|
219
|
+
if is_debian():
|
|
220
|
+
try:
|
|
221
|
+
return run(f'rm -rf "{path}"')
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
# Make sure all files are writeable and dirs executable to remove
|
|
225
|
+
try:
|
|
226
|
+
chmod_r(path, 0o777)
|
|
227
|
+
except PermissionError:
|
|
228
|
+
pass # todo log
|
|
229
|
+
# check if the file is either a normal file, or, e.g., a fifo
|
|
230
|
+
exists_but_non_dir = os.path.exists(path) and not os.path.isdir(path)
|
|
231
|
+
if os.path.isfile(path) or exists_but_non_dir:
|
|
232
|
+
os.remove(path)
|
|
233
|
+
else:
|
|
234
|
+
shutil.rmtree(path)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def cp_r(src: str, dst: str, rm_dest_on_conflict=False, ignore_copystat_errors=False, **kwargs):
|
|
238
|
+
"""Recursively copies file/directory"""
|
|
239
|
+
# attention: this patch is not threadsafe
|
|
240
|
+
copystat_orig = shutil.copystat
|
|
241
|
+
if ignore_copystat_errors:
|
|
242
|
+
|
|
243
|
+
def _copystat(*args, **kwargs):
|
|
244
|
+
try:
|
|
245
|
+
return copystat_orig(*args, **kwargs)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
shutil.copystat = _copystat
|
|
250
|
+
try:
|
|
251
|
+
if os.path.isfile(src):
|
|
252
|
+
if os.path.isdir(dst):
|
|
253
|
+
dst = os.path.join(dst, os.path.basename(src))
|
|
254
|
+
return shutil.copyfile(src, dst)
|
|
255
|
+
if "dirs_exist_ok" in inspect.getfullargspec(shutil.copytree).args:
|
|
256
|
+
kwargs["dirs_exist_ok"] = True
|
|
257
|
+
try:
|
|
258
|
+
return shutil.copytree(src, dst, **kwargs)
|
|
259
|
+
except FileExistsError:
|
|
260
|
+
if rm_dest_on_conflict:
|
|
261
|
+
rm_rf(dst)
|
|
262
|
+
return shutil.copytree(src, dst, **kwargs)
|
|
263
|
+
raise
|
|
264
|
+
except Exception as e:
|
|
265
|
+
|
|
266
|
+
def _info(_path):
|
|
267
|
+
return f"{_path} (file={os.path.isfile(_path)}, symlink={os.path.islink(_path)})"
|
|
268
|
+
|
|
269
|
+
LOG.debug("Error copying files from %s to %s: %s", _info(src), _info(dst), e)
|
|
270
|
+
raise
|
|
271
|
+
finally:
|
|
272
|
+
shutil.copystat = copystat_orig
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def disk_usage(path: str) -> int:
|
|
276
|
+
"""Return the disk usage of the given file or directory."""
|
|
277
|
+
|
|
278
|
+
if not os.path.exists(path):
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
if os.path.isfile(path):
|
|
282
|
+
return os.path.getsize(path)
|
|
283
|
+
|
|
284
|
+
total_size = 0
|
|
285
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
286
|
+
for f in filenames:
|
|
287
|
+
fp = os.path.join(dirpath, f)
|
|
288
|
+
# skip if it is symbolic link
|
|
289
|
+
if not os.path.islink(fp):
|
|
290
|
+
total_size += os.path.getsize(fp)
|
|
291
|
+
return total_size
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def file_exists_not_empty(path: str) -> bool:
|
|
295
|
+
"""Return whether the given file or directory exists and is non-empty (i.e., >0 bytes content)"""
|
|
296
|
+
return path and disk_usage(path) > 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cleanup_tmp_files():
|
|
300
|
+
for tmp in TMP_FILES:
|
|
301
|
+
try:
|
|
302
|
+
rm_rf(tmp)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass # file likely doesn't exist, or permission denied
|
|
305
|
+
del TMP_FILES[:]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def new_tmp_file(suffix: str | None = None, dir: str | None = None) -> str:
|
|
309
|
+
"""Return a path to a new temporary file."""
|
|
310
|
+
tmp_file, tmp_path = tempfile.mkstemp(suffix=suffix, dir=dir)
|
|
311
|
+
os.close(tmp_file)
|
|
312
|
+
TMP_FILES.append(tmp_path)
|
|
313
|
+
return tmp_path
|
|
314
|
+
|
|
315
|
+
|
|
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)
|
|
327
|
+
return folder
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Higher-order functional tools."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_safe(_python_lambda, *args, _default=None, **kwargs):
|
|
13
|
+
print_error = kwargs.get("print_error", False)
|
|
14
|
+
try:
|
|
15
|
+
return _python_lambda(*args, **kwargs)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
if print_error:
|
|
18
|
+
LOG.warning("Unable to execute function: %s", e)
|
|
19
|
+
return _default
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def call_safe(
|
|
23
|
+
func: Callable, args: tuple = None, kwargs: dict = None, exception_message: str = None
|
|
24
|
+
) -> Any | None:
|
|
25
|
+
"""
|
|
26
|
+
Call the given function with the given arguments, and if it fails, log the given exception_message.
|
|
27
|
+
If logging.DEBUG is set for the logger, then we also log the traceback.
|
|
28
|
+
|
|
29
|
+
:param func: function to call
|
|
30
|
+
:param args: arguments to pass
|
|
31
|
+
:param kwargs: keyword arguments to pass
|
|
32
|
+
:param exception_message: message to log on exception
|
|
33
|
+
:return: whatever the func returns
|
|
34
|
+
"""
|
|
35
|
+
if exception_message is None:
|
|
36
|
+
exception_message = f"error calling function {func.__name__}"
|
|
37
|
+
if args is None:
|
|
38
|
+
args = ()
|
|
39
|
+
if kwargs is None:
|
|
40
|
+
kwargs = {}
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return func(*args, **kwargs)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
46
|
+
LOG.exception(exception_message)
|
|
47
|
+
else:
|
|
48
|
+
LOG.warning("%s: %s", exception_message, e)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def prevent_stack_overflow(match_parameters=False):
|
|
52
|
+
"""Function decorator to protect a function from stack overflows -
|
|
53
|
+
raises an exception if a (potential) infinite recursion is detected."""
|
|
54
|
+
|
|
55
|
+
def _decorator(wrapped):
|
|
56
|
+
@functools.wraps(wrapped)
|
|
57
|
+
def func(*args, **kwargs):
|
|
58
|
+
def _matches(frame):
|
|
59
|
+
if frame.function != wrapped.__name__:
|
|
60
|
+
return False
|
|
61
|
+
frame = frame.frame
|
|
62
|
+
|
|
63
|
+
if not match_parameters:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# construct dict of arguments this stack frame has been called with
|
|
67
|
+
prev_call_args = {
|
|
68
|
+
frame.f_code.co_varnames[i]: frame.f_locals[frame.f_code.co_varnames[i]]
|
|
69
|
+
for i in range(frame.f_code.co_argcount)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# construct dict of arguments the original function has been called with
|
|
73
|
+
sig = inspect.signature(wrapped)
|
|
74
|
+
this_call_args = dict(zip(sig.parameters.keys(), args, strict=False))
|
|
75
|
+
this_call_args.update(kwargs)
|
|
76
|
+
|
|
77
|
+
return prev_call_args == this_call_args
|
|
78
|
+
|
|
79
|
+
matching_frames = [frame[2] for frame in inspect.stack(context=1) if _matches(frame)]
|
|
80
|
+
if matching_frames:
|
|
81
|
+
raise RecursionError("(Potential) infinite recursion detected")
|
|
82
|
+
return wrapped(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
return func
|
|
85
|
+
|
|
86
|
+
return _decorator
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def empty_context_manager():
|
|
90
|
+
import contextlib
|
|
91
|
+
|
|
92
|
+
return contextlib.nullcontext()
|