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,187 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from localstack_cli.utils.net import is_port_open
|
|
6
|
+
from localstack_cli.utils.sync import poll_condition
|
|
7
|
+
from localstack_cli.utils.threads import FuncThread, start_thread
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StopServer(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Server(abc.ABC):
|
|
17
|
+
"""
|
|
18
|
+
A Server implements the lifecycle of a server running in a thread.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, port: int, host: str = "localhost") -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._thread: FuncThread | None = None
|
|
24
|
+
|
|
25
|
+
self._lifecycle_lock = threading.RLock()
|
|
26
|
+
self._stopped = threading.Event()
|
|
27
|
+
self._started = threading.Event()
|
|
28
|
+
|
|
29
|
+
self._host = host
|
|
30
|
+
self._port = port
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def host(self):
|
|
34
|
+
return self._host
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def port(self):
|
|
38
|
+
return self._port
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def protocol(self):
|
|
42
|
+
return "http"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def url(self):
|
|
46
|
+
return f"{self.protocol}://{self.host}:{self.port}"
|
|
47
|
+
|
|
48
|
+
def get_error(self) -> Exception | None:
|
|
49
|
+
"""
|
|
50
|
+
If the thread running the server returned with an Exception, then this function will return that exception.
|
|
51
|
+
"""
|
|
52
|
+
if not self._started.is_set():
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
future = self._thread.result_future
|
|
56
|
+
if future.done():
|
|
57
|
+
return future.exception()
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def wait_is_up(self, timeout: float = None) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Waits until the server is started and is_up returns true.
|
|
63
|
+
|
|
64
|
+
:param timeout: the time in seconds to wait before returning false. If timeout is None, then wait indefinitely.
|
|
65
|
+
:returns: true if the server is up, false if not or the timeout was reached while waiting.
|
|
66
|
+
"""
|
|
67
|
+
# first wait until the started event was called
|
|
68
|
+
self._started.wait(timeout=timeout)
|
|
69
|
+
# then poll the health check
|
|
70
|
+
return poll_condition(self.is_up, timeout=timeout)
|
|
71
|
+
|
|
72
|
+
def is_running(self) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Checks whether the thread holding the server is running. The server may be running but not healthy (
|
|
75
|
+
is_running == True, is_up == False).
|
|
76
|
+
|
|
77
|
+
:returns: true if the server thread is running
|
|
78
|
+
"""
|
|
79
|
+
if not self._started.is_set():
|
|
80
|
+
return False
|
|
81
|
+
if self._stopped.is_set():
|
|
82
|
+
return False
|
|
83
|
+
return self._thread.running
|
|
84
|
+
|
|
85
|
+
def is_up(self) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Checks whether the server is up by executing the health check function.
|
|
88
|
+
|
|
89
|
+
:returns: false if the server has not been started or if the health check failed, true otherwise
|
|
90
|
+
"""
|
|
91
|
+
if not self._started.is_set():
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
return True if self.health() else False
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def shutdown(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Attempts to shut down the server by calling the internal do_shutdown method. It only does this if the server
|
|
102
|
+
has been started. Repeated calls to this function have no effect.
|
|
103
|
+
|
|
104
|
+
:raises RuntimeError: shutdown was called before start
|
|
105
|
+
"""
|
|
106
|
+
with self._lifecycle_lock:
|
|
107
|
+
if not self._started.is_set():
|
|
108
|
+
raise RuntimeError("cannot shutdown server before it is started")
|
|
109
|
+
if self._stopped.is_set():
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
self._thread.stop()
|
|
113
|
+
self._stopped.set()
|
|
114
|
+
self.do_shutdown()
|
|
115
|
+
|
|
116
|
+
def start(self) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Starts the server by calling the internal do_run method in a new thread, and then returns True. Repeated
|
|
119
|
+
calls to this function have no effect but return False.
|
|
120
|
+
|
|
121
|
+
:return: True if the server was started in this call, False if the server was already started previously
|
|
122
|
+
"""
|
|
123
|
+
with self._lifecycle_lock:
|
|
124
|
+
if self._started.is_set():
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
self._thread = self.do_start_thread()
|
|
128
|
+
self._started.set()
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
def join(self, timeout=None):
|
|
132
|
+
"""
|
|
133
|
+
Waits for the given amount of time until the thread running the server returns. If the server hasn't started
|
|
134
|
+
yet, it first waits for the server to start.
|
|
135
|
+
|
|
136
|
+
:params: the time in seconds to wait. If None then wait indefinitely.
|
|
137
|
+
:raises TimeoutError: If the server didn't shut down before the given timeout.
|
|
138
|
+
"""
|
|
139
|
+
if not self._started.is_set():
|
|
140
|
+
raise RuntimeError("cannot join server before it is started")
|
|
141
|
+
|
|
142
|
+
if not self._started.wait(timeout):
|
|
143
|
+
raise TimeoutError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
self._thread.result_future.result(timeout)
|
|
147
|
+
except TimeoutError:
|
|
148
|
+
raise
|
|
149
|
+
except Exception:
|
|
150
|
+
# Future.result() will re-raise the exception that was raised in the thread
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
def health(self):
|
|
154
|
+
"""
|
|
155
|
+
Runs a health check on the server. The default implementation performs is_port_open on the server URL.
|
|
156
|
+
"""
|
|
157
|
+
return is_port_open(self.url)
|
|
158
|
+
|
|
159
|
+
def do_start_thread(self) -> FuncThread:
|
|
160
|
+
"""
|
|
161
|
+
Creates and starts the thread running the server. By default, it calls the do_run method in a FuncThread, but
|
|
162
|
+
can be overridden to if the subclass wants to return its own thread.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def _run(*_):
|
|
166
|
+
try:
|
|
167
|
+
return self.do_run()
|
|
168
|
+
except StopServer:
|
|
169
|
+
LOG.debug("stopping server %s", self.url)
|
|
170
|
+
finally:
|
|
171
|
+
self._stopped.set()
|
|
172
|
+
|
|
173
|
+
return start_thread(_run, name=f"server-{self.__class__.__name__}")
|
|
174
|
+
|
|
175
|
+
def do_run(self):
|
|
176
|
+
"""
|
|
177
|
+
Runs the server (blocking method). (Needs to be overridden by subclasses of do_start_thread is not overridden).
|
|
178
|
+
|
|
179
|
+
:raises StopServer: can be raised by the subclass to indicate the server should be stopped.
|
|
180
|
+
"""
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
def do_shutdown(self):
|
|
184
|
+
"""
|
|
185
|
+
Called when shutdown() is performed. (Should be overridden by subclasses).
|
|
186
|
+
"""
|
|
187
|
+
pass
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from localstack_cli import config
|
|
5
|
+
from localstack_cli.constants import API_ENDPOINT, ASSETS_ENDPOINT
|
|
6
|
+
from localstack_cli.utils.crypto import generate_ssl_cert
|
|
7
|
+
from localstack_cli.utils.http import download
|
|
8
|
+
from localstack_cli.utils.time import now
|
|
9
|
+
from localstack_cli.version import __version__ as version
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Download URLs
|
|
14
|
+
SSL_CERT_URL = f"{ASSETS_ENDPOINT}/local-certs/localstack.cert.key?version={version}"
|
|
15
|
+
SSL_CERT_URL_FALLBACK = f"{API_ENDPOINT}/proxy/localstack.cert.key?version={version}"
|
|
16
|
+
|
|
17
|
+
# path for test certificate
|
|
18
|
+
_SERVER_CERT_PEM_FILE = "server.test.pem"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def install_predefined_cert_if_available():
|
|
22
|
+
try:
|
|
23
|
+
if config.SKIP_SSL_CERT_DOWNLOAD:
|
|
24
|
+
LOG.debug("Skipping download of local SSL cert, as SKIP_SSL_CERT_DOWNLOAD=1")
|
|
25
|
+
return
|
|
26
|
+
setup_ssl_cert()
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def setup_ssl_cert() -> None:
|
|
32
|
+
target_file = get_cert_pem_file_path()
|
|
33
|
+
|
|
34
|
+
# cache file for 6 hours (non-enterprise) or forever (enterprise)
|
|
35
|
+
if os.path.exists(target_file):
|
|
36
|
+
cache_duration_secs = 24 * 60 * 60
|
|
37
|
+
mod_time = os.path.getmtime(target_file)
|
|
38
|
+
if mod_time > (now() - cache_duration_secs):
|
|
39
|
+
LOG.debug("Using cached SSL certificate (less than 6hrs since last update).")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# download certificate from GitHub artifacts
|
|
43
|
+
LOG.debug("Attempting to download local SSL certificate file")
|
|
44
|
+
|
|
45
|
+
# apply timeout (and fall back to using self-signed certs)
|
|
46
|
+
timeout = 5 # slightly higher timeout for our proxy
|
|
47
|
+
try:
|
|
48
|
+
download(SSL_CERT_URL, target_file, timeout=timeout, quiet=True)
|
|
49
|
+
LOG.debug("SSL certificate downloaded successfully")
|
|
50
|
+
except Exception:
|
|
51
|
+
# try fallback URL, directly from our API proxy
|
|
52
|
+
try:
|
|
53
|
+
download(SSL_CERT_URL_FALLBACK, target_file, timeout=timeout, quiet=True)
|
|
54
|
+
LOG.debug("SSL certificate downloaded successfully")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
LOG.info(
|
|
57
|
+
"Unable to download local test SSL certificate from %s to %s (using self-signed cert as fallback): %s",
|
|
58
|
+
SSL_CERT_URL_FALLBACK,
|
|
59
|
+
target_file,
|
|
60
|
+
e,
|
|
61
|
+
)
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_cert_pem_file_path():
|
|
66
|
+
return config.CUSTOM_SSL_CERT_PATH or os.path.join(config.dirs.cache, _SERVER_CERT_PEM_FILE)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_ssl_cert(serial_number=None):
|
|
70
|
+
cert_pem_file = get_cert_pem_file_path()
|
|
71
|
+
return generate_ssl_cert(cert_pem_file, serial_number=serial_number)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
import hashlib
|
|
4
|
+
import itertools
|
|
5
|
+
import random
|
|
6
|
+
import re
|
|
7
|
+
import string
|
|
8
|
+
import uuid
|
|
9
|
+
import zlib
|
|
10
|
+
|
|
11
|
+
from localstack_cli.config import DEFAULT_ENCODING
|
|
12
|
+
|
|
13
|
+
_unprintables = (
|
|
14
|
+
range(0x00, 0x09),
|
|
15
|
+
range(0x0A, 0x0A),
|
|
16
|
+
range(0x0B, 0x0D),
|
|
17
|
+
range(0x0E, 0x20),
|
|
18
|
+
range(0xD800, 0xE000),
|
|
19
|
+
range(0xFFFE, 0x10000),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# regular expression for unprintable characters
|
|
23
|
+
# Based on https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html
|
|
24
|
+
# #x9 | #xA | #xD | #x20 to #xD7FF | #xE000 to #xFFFD | #x10000 to #x10FFFF
|
|
25
|
+
REGEX_UNPRINTABLE_CHARS = re.compile(
|
|
26
|
+
f"[{re.escape(''.join(map(chr, itertools.chain(*_unprintables))))}]"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_str(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> str:
|
|
31
|
+
"""If ``obj`` is an instance of ``binary_type``, return
|
|
32
|
+
``obj.decode(encoding, errors)``, otherwise return ``obj``"""
|
|
33
|
+
return obj.decode(encoding, errors) if isinstance(obj, bytes) else obj
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def to_bytes(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> bytes:
|
|
37
|
+
"""If ``obj`` is an instance of ``text_type``, return
|
|
38
|
+
``obj.encode(encoding, errors)``, otherwise return ``obj``"""
|
|
39
|
+
return obj.encode(encoding, errors) if isinstance(obj, str) else obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def truncate(data: str, max_length: int = 100) -> str:
|
|
43
|
+
data = str(data or "")
|
|
44
|
+
return (f"{data[:max_length]}...") if len(data) > max_length else data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_string(s, include_unicode=True, exclude_binary=False):
|
|
48
|
+
if isinstance(s, bytes) and exclude_binary:
|
|
49
|
+
return False
|
|
50
|
+
if isinstance(s, str):
|
|
51
|
+
return True
|
|
52
|
+
if include_unicode and isinstance(s, str):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_string_or_bytes(s):
|
|
58
|
+
return is_string(s) or isinstance(s, str) or isinstance(s, bytes)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_base64(s):
|
|
62
|
+
regex = r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
|
|
63
|
+
return is_string(s) and re.match(regex, s)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_re_camel_to_snake_case = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def camel_to_snake_case(string: str) -> str:
|
|
70
|
+
return _re_camel_to_snake_case.sub(r"_\1", string).replace("__", "_").lower()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def snake_to_camel_case(string: str, capitalize_first: bool = True) -> str:
|
|
74
|
+
components = string.split("_")
|
|
75
|
+
start_idx = 0 if capitalize_first else 1
|
|
76
|
+
components = [x.title() for x in components[start_idx:]]
|
|
77
|
+
return "".join(components)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def hyphen_to_snake_case(string: str) -> str:
|
|
81
|
+
return string.replace("-", "_")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def canonicalize_bool_to_str(val: bool) -> str:
|
|
85
|
+
return "true" if str(val).lower() == "true" else "false"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def convert_to_printable_chars(value: list | dict | str) -> str:
|
|
89
|
+
"""Removes all unprintable characters from the given string."""
|
|
90
|
+
from localstack_cli.utils.objects import recurse_object
|
|
91
|
+
|
|
92
|
+
if isinstance(value, (dict, list)):
|
|
93
|
+
|
|
94
|
+
def _convert(obj, **kwargs):
|
|
95
|
+
if isinstance(obj, str):
|
|
96
|
+
return convert_to_printable_chars(obj)
|
|
97
|
+
return obj
|
|
98
|
+
|
|
99
|
+
return recurse_object(value, _convert)
|
|
100
|
+
|
|
101
|
+
result = REGEX_UNPRINTABLE_CHARS.sub("", value)
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def first_char_to_lower(s: str) -> str:
|
|
106
|
+
return s and f"{s[0].lower()}{s[1:]}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def first_char_to_upper(s: str) -> str:
|
|
110
|
+
return s and f"{s[0].upper()}{s[1:]}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def str_to_bool(value):
|
|
114
|
+
"""Return the boolean value of the given string, or the verbatim value if it is not a string"""
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
true_strings = ["true", "True"]
|
|
117
|
+
return value in true_strings
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def str_insert(string, index, content):
|
|
122
|
+
"""Insert a substring into an existing string at a certain index."""
|
|
123
|
+
return f"{string[:index]}{content}{string[index:]}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def str_remove(string, index, end_index=None):
|
|
127
|
+
"""Remove a substring from an existing string at a certain from-to index range."""
|
|
128
|
+
end_index = end_index or (index + 1)
|
|
129
|
+
return f"{string[:index]}{string[end_index:]}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def str_startswith_ignore_case(value: str, prefix: str) -> bool:
|
|
133
|
+
return value[: len(prefix)].lower() == prefix.lower()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def short_uid() -> str:
|
|
137
|
+
return str(uuid.uuid4())[0:8]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def short_uid_from_seed(seed: str) -> str:
|
|
141
|
+
hash = hashlib.sha1(seed.encode("utf-8")).hexdigest()
|
|
142
|
+
truncated_hash = hash[:32]
|
|
143
|
+
return str(uuid.UUID(truncated_hash))[0:8]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def long_uid() -> str:
|
|
147
|
+
return str(uuid.uuid4())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def md5(string: str | bytes) -> str:
|
|
151
|
+
m = hashlib.md5()
|
|
152
|
+
m.update(to_bytes(string))
|
|
153
|
+
return m.hexdigest()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def checksum_crc32(string: str | bytes) -> str:
|
|
157
|
+
bytes = to_bytes(string)
|
|
158
|
+
checksum = zlib.crc32(bytes)
|
|
159
|
+
return base64.b64encode(checksum.to_bytes(4, "big")).decode()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def checksum_crc32c(string: str | bytes):
|
|
163
|
+
# import botocore locally here to avoid a dependency of the CLI to botocore
|
|
164
|
+
from botocore.httpchecksum import CrtCrc32cChecksum
|
|
165
|
+
|
|
166
|
+
checksum = CrtCrc32cChecksum()
|
|
167
|
+
checksum.update(to_bytes(string))
|
|
168
|
+
return base64.b64encode(checksum.digest()).decode()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def checksum_crc64nvme(string: str | bytes):
|
|
172
|
+
# import botocore locally here to avoid a dependency of the CLI to botocore
|
|
173
|
+
from botocore.httpchecksum import CrtCrc64NvmeChecksum
|
|
174
|
+
|
|
175
|
+
checksum = CrtCrc64NvmeChecksum()
|
|
176
|
+
checksum.update(to_bytes(string))
|
|
177
|
+
return base64.b64encode(checksum.digest()).decode()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def hash_sha1(string: str | bytes) -> str:
|
|
181
|
+
digest = hashlib.sha1(to_bytes(string)).digest()
|
|
182
|
+
return base64.b64encode(digest).decode()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def hash_sha256(string: str | bytes) -> str:
|
|
186
|
+
digest = hashlib.sha256(to_bytes(string)).digest()
|
|
187
|
+
return base64.b64encode(digest).decode()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def base64_to_hex(b64_string: str) -> bytes:
|
|
191
|
+
return binascii.hexlify(base64.b64decode(b64_string))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def base64_decode(data: str | bytes) -> bytes:
|
|
195
|
+
"""Decode base64 data - with optional padding, and able to handle urlsafe encoding (containing -/_)."""
|
|
196
|
+
data = to_str(data)
|
|
197
|
+
missing_padding = len(data) % 4
|
|
198
|
+
if missing_padding != 0:
|
|
199
|
+
data = to_str(data) + "=" * (4 - missing_padding)
|
|
200
|
+
if "-" in data or "_" in data:
|
|
201
|
+
return base64.urlsafe_b64decode(data)
|
|
202
|
+
return base64.b64decode(data)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_random_hex(length: int) -> str:
|
|
206
|
+
return "".join(random.choices(string.hexdigits[:16], k=length)).lower()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def remove_leading_extra_slashes(input: str) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Remove leading extra slashes from the given input string.
|
|
212
|
+
Example: '///foo/bar' -> '/foo/bar'
|
|
213
|
+
"""
|
|
214
|
+
return re.sub(r"^/+", "/", input)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def prepend_with_slash(input: str) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Prepend a slash `/` to a given string if it does not have one already.
|
|
220
|
+
"""
|
|
221
|
+
if not input.startswith("/"):
|
|
222
|
+
return f"/{input}"
|
|
223
|
+
return input
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def key_value_pairs_to_dict(pairs: str, delimiter: str = ",", separator: str = "=") -> dict:
|
|
227
|
+
"""
|
|
228
|
+
Converts a string of key-value pairs to a dictionary.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
pairs (str): A string containing key-value pairs separated by a delimiter.
|
|
232
|
+
delimiter (str): The delimiter used to separate key-value pairs (default is comma ',').
|
|
233
|
+
separator (str): The separator between keys and values (default is '=').
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
dict: A dictionary containing the parsed key-value pairs.
|
|
237
|
+
"""
|
|
238
|
+
splits = [split_pair.partition(separator) for split_pair in pairs.split(delimiter)]
|
|
239
|
+
return {key.strip(): value.strip() for key, _, value in splits}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def token_generator(item: str) -> str:
|
|
243
|
+
base64_bytes = base64.b64encode(item.encode("utf-8"))
|
|
244
|
+
token = base64_bytes.decode("utf-8")
|
|
245
|
+
return token
|