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.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. 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