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,267 @@
|
|
|
1
|
+
"""Concurrency synchronization utilities"""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Literal, TypeVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ShortCircuitWaitException(Exception):
|
|
12
|
+
"""raise to immediately stop waiting, e.g. when an operation permanently failed"""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def wait_until(
|
|
18
|
+
fn: Callable[[], bool],
|
|
19
|
+
wait: float = 1.0,
|
|
20
|
+
max_retries: int = 10,
|
|
21
|
+
strategy: Literal["exponential", "static", "linear"] = "exponential",
|
|
22
|
+
_retries: int = 1,
|
|
23
|
+
_max_wait: float = 240,
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""waits until a given condition is true, rechecking it periodically"""
|
|
26
|
+
assert _retries > 0
|
|
27
|
+
if max_retries < _retries:
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
completed = fn()
|
|
31
|
+
except ShortCircuitWaitException:
|
|
32
|
+
return False
|
|
33
|
+
except Exception:
|
|
34
|
+
completed = False
|
|
35
|
+
|
|
36
|
+
if completed:
|
|
37
|
+
return True
|
|
38
|
+
else:
|
|
39
|
+
if wait > _max_wait:
|
|
40
|
+
return False
|
|
41
|
+
time.sleep(wait)
|
|
42
|
+
next_wait = wait # default: static
|
|
43
|
+
if strategy == "linear":
|
|
44
|
+
next_wait = (wait / _retries) * (_retries + 1)
|
|
45
|
+
elif strategy == "exponential":
|
|
46
|
+
next_wait = wait * 2
|
|
47
|
+
return wait_until(fn, next_wait, max_retries, strategy, _retries + 1, _max_wait)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
T = TypeVar("T")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def retry(function: Callable[..., T], retries=3, sleep=1.0, sleep_before=0, **kwargs) -> T:
|
|
54
|
+
raise_error = None
|
|
55
|
+
if sleep_before > 0:
|
|
56
|
+
time.sleep(sleep_before)
|
|
57
|
+
retries = int(retries)
|
|
58
|
+
for i in range(0, retries + 1):
|
|
59
|
+
try:
|
|
60
|
+
return function(**kwargs)
|
|
61
|
+
except Exception as error:
|
|
62
|
+
raise_error = error
|
|
63
|
+
time.sleep(sleep)
|
|
64
|
+
raise raise_error
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def poll_condition(condition, timeout: float = None, interval: float = 0.5) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Poll evaluates the given condition until a truthy value is returned. It does this every `interval` seconds
|
|
70
|
+
(0.5 by default), until the timeout (in seconds, if any) is reached.
|
|
71
|
+
|
|
72
|
+
Poll returns True once `condition()` returns a truthy value, or False if the timeout is reached.
|
|
73
|
+
"""
|
|
74
|
+
remaining = 0
|
|
75
|
+
if timeout is not None:
|
|
76
|
+
remaining = timeout
|
|
77
|
+
|
|
78
|
+
while not condition():
|
|
79
|
+
if timeout is not None:
|
|
80
|
+
remaining -= interval
|
|
81
|
+
|
|
82
|
+
if remaining <= 0:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
time.sleep(interval)
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def synchronized(lock=None):
|
|
91
|
+
"""
|
|
92
|
+
Synchronization decorator as described in
|
|
93
|
+
http://blog.dscpl.com.au/2014/01/the-missing-synchronized-decorator.html.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def _decorator(wrapped):
|
|
97
|
+
@functools.wraps(wrapped)
|
|
98
|
+
def _wrapper(*args, **kwargs):
|
|
99
|
+
with lock:
|
|
100
|
+
return wrapped(*args, **kwargs)
|
|
101
|
+
|
|
102
|
+
return _wrapper
|
|
103
|
+
|
|
104
|
+
return _decorator
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def sleep_forever():
|
|
108
|
+
while True:
|
|
109
|
+
time.sleep(1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SynchronizedDefaultDict(defaultdict):
|
|
113
|
+
def __init__(self, *args, **kwargs):
|
|
114
|
+
super().__init__(*args, **kwargs)
|
|
115
|
+
self._lock = threading.RLock()
|
|
116
|
+
|
|
117
|
+
def fromkeys(self, keys, value=None):
|
|
118
|
+
with self._lock:
|
|
119
|
+
return super().fromkeys(keys, value)
|
|
120
|
+
|
|
121
|
+
def __getitem__(self, key):
|
|
122
|
+
with self._lock:
|
|
123
|
+
return super().__getitem__(key)
|
|
124
|
+
|
|
125
|
+
def __setitem__(self, key, value):
|
|
126
|
+
with self._lock:
|
|
127
|
+
super().__setitem__(key, value)
|
|
128
|
+
|
|
129
|
+
def __delitem__(self, key):
|
|
130
|
+
with self._lock:
|
|
131
|
+
super().__delitem__(key)
|
|
132
|
+
|
|
133
|
+
def __iter__(self):
|
|
134
|
+
with self._lock:
|
|
135
|
+
return super().__iter__()
|
|
136
|
+
|
|
137
|
+
def __len__(self):
|
|
138
|
+
with self._lock:
|
|
139
|
+
return super().__len__()
|
|
140
|
+
|
|
141
|
+
def __str__(self):
|
|
142
|
+
with self._lock:
|
|
143
|
+
return super().__str__()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Once:
|
|
147
|
+
"""
|
|
148
|
+
An object that will perform an action exactly once.
|
|
149
|
+
Inspired by Golang's [sync.Once](https://pkg.go.dev/sync#Once) operation.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### Example 1
|
|
153
|
+
|
|
154
|
+
Multiple threads using `Once::do` to ensure only 1 line is printed.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import threading
|
|
158
|
+
import time
|
|
159
|
+
import random
|
|
160
|
+
|
|
161
|
+
greet_once = Once()
|
|
162
|
+
def greet():
|
|
163
|
+
print("This should happen only once.")
|
|
164
|
+
|
|
165
|
+
greet_threads = []
|
|
166
|
+
for _ in range(10):
|
|
167
|
+
t = threading.Thread(target=lambda: greet_once.do(greet))
|
|
168
|
+
greet_threads.append(t)
|
|
169
|
+
t.start()
|
|
170
|
+
|
|
171
|
+
for t in greet_threads:
|
|
172
|
+
t.join()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
### Example 2
|
|
177
|
+
|
|
178
|
+
Ensuring idemponent calling to prevent exceptions on multiple calls.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import os
|
|
182
|
+
|
|
183
|
+
class Service:
|
|
184
|
+
close_once: sync.Once
|
|
185
|
+
|
|
186
|
+
def start(self):
|
|
187
|
+
with open("my-service.txt) as f:
|
|
188
|
+
myfile.write("Started service")
|
|
189
|
+
|
|
190
|
+
def close(self):
|
|
191
|
+
# Ensure we only ever delete the file once on close
|
|
192
|
+
self.close_once.do(lambda: os.remove("my-service.txt"))
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
_is_done: bool = False
|
|
200
|
+
_mu: threading.Lock = threading.Lock()
|
|
201
|
+
|
|
202
|
+
def do(self, fn: Callable[[], None]):
|
|
203
|
+
"""
|
|
204
|
+
`do` calls the function `fn()` if-and-only-if `do` has never been called before.
|
|
205
|
+
|
|
206
|
+
This ensures idempotent and thread-safe execution.
|
|
207
|
+
|
|
208
|
+
If the function raises an exception, `do` considers `fn` as done, where subsequent calls are still no-ops.
|
|
209
|
+
"""
|
|
210
|
+
if self._is_done:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
with self._mu:
|
|
214
|
+
if not self._is_done:
|
|
215
|
+
try:
|
|
216
|
+
fn()
|
|
217
|
+
finally:
|
|
218
|
+
self._is_done = True
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def once_func(fn: Callable[..., T]) -> Callable[..., T | None]:
|
|
222
|
+
"""
|
|
223
|
+
Wraps and returns a function that can only ever execute once.
|
|
224
|
+
|
|
225
|
+
The first call to the returned function will permanently set the result.
|
|
226
|
+
If the wrapped function raises an exception, this will be re-raised on each subsequent call.
|
|
227
|
+
|
|
228
|
+
This function can be used either as a decorator or called directly.
|
|
229
|
+
|
|
230
|
+
Direct usage:
|
|
231
|
+
```python
|
|
232
|
+
delete_file = once_func(os.remove)
|
|
233
|
+
|
|
234
|
+
delete_file("myfile.txt") # deletes the file
|
|
235
|
+
delete_file("myfile.txt") # does nothing
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
As a decorator:
|
|
239
|
+
```python
|
|
240
|
+
@once_func
|
|
241
|
+
def delete_file():
|
|
242
|
+
os.remove("myfile.txt")
|
|
243
|
+
|
|
244
|
+
delete_file() # deletes the file
|
|
245
|
+
delete_file() # does nothing
|
|
246
|
+
```
|
|
247
|
+
"""
|
|
248
|
+
once = Once()
|
|
249
|
+
|
|
250
|
+
result, exception = None, None
|
|
251
|
+
|
|
252
|
+
def _do(*args, **kwargs):
|
|
253
|
+
nonlocal result, exception
|
|
254
|
+
try:
|
|
255
|
+
result = fn(*args, **kwargs)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
exception = e
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
@functools.wraps(fn)
|
|
261
|
+
def wrapper(*args, **kwargs):
|
|
262
|
+
once.do(lambda: _do(*args, **kwargs))
|
|
263
|
+
if exception is not None:
|
|
264
|
+
raise exception
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
return wrapper
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import traceback
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from concurrent.futures import Future
|
|
8
|
+
from multiprocessing.dummy import Pool
|
|
9
|
+
|
|
10
|
+
LOG = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# arrays for temporary threads and resources
|
|
13
|
+
TMP_THREADS = []
|
|
14
|
+
TMP_PROCESSES = []
|
|
15
|
+
|
|
16
|
+
counter_lock = threading.Lock()
|
|
17
|
+
counter = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FuncThread(threading.Thread):
|
|
21
|
+
"""Helper class to run a Python function in a background thread."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
func,
|
|
26
|
+
params=None,
|
|
27
|
+
quiet=False,
|
|
28
|
+
on_stop: Callable[["FuncThread"], None] = None,
|
|
29
|
+
name: str | None = None,
|
|
30
|
+
daemon=True,
|
|
31
|
+
):
|
|
32
|
+
global counter
|
|
33
|
+
global counter_lock
|
|
34
|
+
|
|
35
|
+
if name:
|
|
36
|
+
with counter_lock:
|
|
37
|
+
counter += 1
|
|
38
|
+
thread_counter_current = counter
|
|
39
|
+
|
|
40
|
+
threading.Thread.__init__(
|
|
41
|
+
self, name=f"{name}-functhread{thread_counter_current}", daemon=daemon
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
threading.Thread.__init__(self, daemon=daemon)
|
|
45
|
+
|
|
46
|
+
self.params = params
|
|
47
|
+
self.func = func
|
|
48
|
+
self.quiet = quiet
|
|
49
|
+
self.result_future = Future()
|
|
50
|
+
self._stop_event = threading.Event()
|
|
51
|
+
self.on_stop = on_stop
|
|
52
|
+
|
|
53
|
+
def run(self):
|
|
54
|
+
result = None
|
|
55
|
+
try:
|
|
56
|
+
kwargs = {}
|
|
57
|
+
argspec = inspect.getfullargspec(self.func)
|
|
58
|
+
if argspec.varkw or "_thread" in (argspec.args or []) + (argspec.kwonlyargs or []):
|
|
59
|
+
kwargs["_thread"] = self
|
|
60
|
+
result = self.func(self.params, **kwargs)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.result_future.set_exception(e)
|
|
63
|
+
result = e
|
|
64
|
+
if not self.quiet:
|
|
65
|
+
LOG.info(
|
|
66
|
+
"Thread run method %s(%s) failed: %s %s",
|
|
67
|
+
self.func,
|
|
68
|
+
self.params,
|
|
69
|
+
e,
|
|
70
|
+
traceback.format_exc(),
|
|
71
|
+
)
|
|
72
|
+
finally:
|
|
73
|
+
try:
|
|
74
|
+
self.result_future.set_result(result)
|
|
75
|
+
pass
|
|
76
|
+
except concurrent.futures.InvalidStateError as e:
|
|
77
|
+
# this can happen on shutdown if the task is already canceled
|
|
78
|
+
LOG.debug(e)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def running(self):
|
|
82
|
+
return not self._stop_event.is_set()
|
|
83
|
+
|
|
84
|
+
def stop(self, quiet: bool = False) -> None:
|
|
85
|
+
self._stop_event.set()
|
|
86
|
+
|
|
87
|
+
if self.on_stop:
|
|
88
|
+
try:
|
|
89
|
+
self.on_stop(self)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
LOG.warning("error while calling on_stop callback: %s", e)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def start_thread(method, *args, **kwargs) -> FuncThread: # TODO: find all usages and add names...
|
|
95
|
+
"""Start the given method in a background thread, and add the thread to the TMP_THREADS shutdown hook"""
|
|
96
|
+
_shutdown_hook = kwargs.pop("_shutdown_hook", True)
|
|
97
|
+
if not kwargs.get("name"):
|
|
98
|
+
LOG.debug(
|
|
99
|
+
"start_thread called without providing a custom name"
|
|
100
|
+
) # technically we should add a new level here for *internal* warnings
|
|
101
|
+
kwargs.setdefault("name", method.__name__)
|
|
102
|
+
thread = FuncThread(method, *args, **kwargs)
|
|
103
|
+
thread.start()
|
|
104
|
+
if _shutdown_hook:
|
|
105
|
+
TMP_THREADS.append(thread)
|
|
106
|
+
return thread
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def start_worker_thread(method, *args, **kwargs):
|
|
110
|
+
kwargs.setdefault("name", "start_worker_thread")
|
|
111
|
+
return start_thread(method, *args, _shutdown_hook=False, **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cleanup_threads_and_processes(quiet=True):
|
|
115
|
+
from localstack_cli.utils.run import kill_process_tree
|
|
116
|
+
|
|
117
|
+
for thread in TMP_THREADS:
|
|
118
|
+
if thread:
|
|
119
|
+
try:
|
|
120
|
+
if hasattr(thread, "shutdown"):
|
|
121
|
+
thread.shutdown()
|
|
122
|
+
continue
|
|
123
|
+
if hasattr(thread, "kill"):
|
|
124
|
+
thread.kill()
|
|
125
|
+
continue
|
|
126
|
+
thread.stop(quiet=quiet)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
LOG.debug("[shutdown] Error stopping thread %s: %s", thread, e)
|
|
129
|
+
if not thread.daemon:
|
|
130
|
+
LOG.warning(
|
|
131
|
+
"[shutdown] Non-daemon thread %s may block localstack shutdown", thread
|
|
132
|
+
)
|
|
133
|
+
for proc in TMP_PROCESSES:
|
|
134
|
+
try:
|
|
135
|
+
kill_process_tree(proc.pid)
|
|
136
|
+
# proc.terminate()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
LOG.debug("[shutdown] Error cleaning up process tree %s: %s", proc, e)
|
|
139
|
+
# clean up async tasks
|
|
140
|
+
try:
|
|
141
|
+
import asyncio
|
|
142
|
+
|
|
143
|
+
for task in asyncio.all_tasks():
|
|
144
|
+
try:
|
|
145
|
+
task.cancel()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
LOG.debug("[shutdown] Error cancelling asyncio task %s: %s", task, e)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
LOG.debug("[shutdown] Done cleaning up threads / processes / tasks")
|
|
151
|
+
# clear lists
|
|
152
|
+
TMP_THREADS.clear()
|
|
153
|
+
TMP_PROCESSES.clear()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parallelize(func: Callable, arr: list, size: int = None):
|
|
157
|
+
if not size:
|
|
158
|
+
size = len(arr)
|
|
159
|
+
if size <= 0:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
with Pool(size) as pool:
|
|
163
|
+
return pool.map(func, arr)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import date, datetime, timezone, tzinfo
|
|
3
|
+
from zoneinfo import ZoneInfo
|
|
4
|
+
|
|
5
|
+
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
6
|
+
TIMESTAMP_FORMAT_TZ = "%Y-%m-%dT%H:%M:%SZ"
|
|
7
|
+
TIMESTAMP_FORMAT_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
8
|
+
TIMESTAMP_READABLE_FORMAT = "%d/%b/%Y:%H:%M:%S %z"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def isoformat_milliseconds(t) -> str:
|
|
12
|
+
try:
|
|
13
|
+
return t.isoformat(timespec="milliseconds")
|
|
14
|
+
except TypeError:
|
|
15
|
+
return t.isoformat()[:-3]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def timestamp(time=None, format: str = TIMESTAMP_FORMAT) -> str:
|
|
19
|
+
if not time:
|
|
20
|
+
time = datetime.utcnow()
|
|
21
|
+
if isinstance(time, (int, float)):
|
|
22
|
+
time = datetime.fromtimestamp(time)
|
|
23
|
+
return time.strftime(format)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def timestamp_millis(time=None) -> str:
|
|
27
|
+
microsecond_time = timestamp(time=time, format=TIMESTAMP_FORMAT_MICROS)
|
|
28
|
+
# truncating microseconds to milliseconds, while leaving the "Z" indicator
|
|
29
|
+
return microsecond_time[:-4] + microsecond_time[-1]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def iso1806_to_epoch(t: str) -> float:
|
|
33
|
+
return datetime.fromisoformat(t).timestamp()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def epoch_to_iso1806(ts: int) -> str:
|
|
37
|
+
return datetime.utcfromtimestamp(ts).isoformat()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def epoch_timestamp() -> float:
|
|
41
|
+
return time.time()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_timestamp(ts_str: str) -> datetime:
|
|
45
|
+
"""
|
|
46
|
+
Parse the incoming date string into a timezone aware datetime object
|
|
47
|
+
:param ts_str:
|
|
48
|
+
:return:
|
|
49
|
+
"""
|
|
50
|
+
for ts_format in [
|
|
51
|
+
TIMESTAMP_FORMAT,
|
|
52
|
+
TIMESTAMP_FORMAT_TZ,
|
|
53
|
+
TIMESTAMP_FORMAT_MICROS,
|
|
54
|
+
TIMESTAMP_READABLE_FORMAT,
|
|
55
|
+
]:
|
|
56
|
+
try:
|
|
57
|
+
value = datetime.strptime(ts_str, ts_format)
|
|
58
|
+
if value.tzinfo is None:
|
|
59
|
+
value = value.replace(tzinfo=ZoneInfo("UTC"))
|
|
60
|
+
return value
|
|
61
|
+
except ValueError:
|
|
62
|
+
pass
|
|
63
|
+
raise Exception(f"Unable to parse timestamp string with any known formats: {ts_str}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def now(millis: bool = False, tz: tzinfo | None = None) -> int:
|
|
67
|
+
return mktime(datetime.now(tz=tz), millis=millis)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def now_utc(millis: bool = False) -> int:
|
|
71
|
+
return now(millis, timezone.utc)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def today_no_time() -> int:
|
|
75
|
+
return mktime(datetime.combine(date.today(), datetime.min.time()))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mktime(ts: datetime, millis: bool = False) -> int:
|
|
79
|
+
if millis:
|
|
80
|
+
return int(ts.timestamp() * 1000)
|
|
81
|
+
return int(ts.timestamp())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from localstack_cli import config
|
|
2
|
+
from localstack_cli.config import HostAndPort
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def path_from_url(url: str) -> str:
|
|
6
|
+
return f"/{url.partition('://')[2].partition('/')[2]}" if "://" in url else url
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def hostname_from_url(url: str) -> str:
|
|
10
|
+
return url.split("://")[-1].split("/")[0].split(":")[0]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def localstack_host(custom_port: int | None = None) -> HostAndPort:
|
|
14
|
+
"""
|
|
15
|
+
Determine the host and port to return to the user based on:
|
|
16
|
+
- the user's configuration (e.g environment variable overrides)
|
|
17
|
+
- the defaults of the system
|
|
18
|
+
"""
|
|
19
|
+
port = custom_port or config.LOCALSTACK_HOST.port
|
|
20
|
+
host = config.LOCALSTACK_HOST.host
|
|
21
|
+
return HostAndPort(host=host, port=port)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VirtualEnvironment:
|
|
10
|
+
"""
|
|
11
|
+
Encapsulates methods to operate and navigate on a python virtual environment.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, venv_dir: Union[str, os.PathLike]):
|
|
15
|
+
self._venv_dir = venv_dir
|
|
16
|
+
|
|
17
|
+
def create(self) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Uses the virtualenv cli to create the virtual environment.
|
|
20
|
+
:return:
|
|
21
|
+
"""
|
|
22
|
+
self.venv_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
from venv import main
|
|
24
|
+
|
|
25
|
+
main([str(self.venv_dir)])
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def exists(self) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Checks whether the virtual environment exists by checking whether the site-package directory of the venv exists.
|
|
31
|
+
:return: the if the venv exists
|
|
32
|
+
:raises NotADirectoryError: if the venv path exists but is not a directory
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return True if self.site_dir else False
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
@cached_property
|
|
40
|
+
def venv_dir(self) -> Path:
|
|
41
|
+
"""
|
|
42
|
+
Returns the path of the virtual environment directory
|
|
43
|
+
:return: the path to the venv
|
|
44
|
+
"""
|
|
45
|
+
return Path(self._venv_dir).absolute()
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def site_dir(self) -> Path:
|
|
49
|
+
"""
|
|
50
|
+
Resolves and returns the site-packages directory of the virtual environment. Once resolved successfully the
|
|
51
|
+
result is cached.
|
|
52
|
+
|
|
53
|
+
:return: the path to the site-packages dir.
|
|
54
|
+
:raise FileNotFoundError: if the venv does not exist or the site-packages could not be found, or there are
|
|
55
|
+
multiple lib/python* directories.
|
|
56
|
+
:raise NotADirectoryError: if the venv is not a directory
|
|
57
|
+
"""
|
|
58
|
+
venv = self.venv_dir
|
|
59
|
+
|
|
60
|
+
if not venv.exists():
|
|
61
|
+
raise FileNotFoundError(f"expected venv directory to exist at {venv}")
|
|
62
|
+
|
|
63
|
+
if not venv.is_dir():
|
|
64
|
+
raise NotADirectoryError(f"expected {venv} to be a directory")
|
|
65
|
+
|
|
66
|
+
matches = list(venv.glob("lib/python*/site-packages"))
|
|
67
|
+
|
|
68
|
+
if not matches:
|
|
69
|
+
raise FileNotFoundError(f"could not find site-packages directory in {venv}")
|
|
70
|
+
|
|
71
|
+
if len(matches) > 1:
|
|
72
|
+
raise FileNotFoundError(f"multiple python versions found in {venv}: {matches}")
|
|
73
|
+
|
|
74
|
+
return matches[0]
|
|
75
|
+
|
|
76
|
+
def inject_to_sys_path(self) -> None:
|
|
77
|
+
path = str(self.site_dir)
|
|
78
|
+
if path and path not in sys.path:
|
|
79
|
+
sys.path.append(path)
|
|
80
|
+
|
|
81
|
+
def add_pth(self, name, path: Union[str, os.PathLike, "VirtualEnvironment"]) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Add a <name>.pth file into the virtual environment and append the given path to it. Does nothing if the path
|
|
84
|
+
is already in the file.
|
|
85
|
+
|
|
86
|
+
:param name: the name of the path file (without the .pth extensions)
|
|
87
|
+
:param path: the path to be appended
|
|
88
|
+
"""
|
|
89
|
+
pth_file = self.site_dir / f"{name}.pth"
|
|
90
|
+
|
|
91
|
+
if isinstance(path, VirtualEnvironment):
|
|
92
|
+
path = path.site_dir
|
|
93
|
+
|
|
94
|
+
line = io.text_encoding(str(path)) + "\n"
|
|
95
|
+
|
|
96
|
+
if pth_file.exists() and line in pth_file.read_text():
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
with open(pth_file, "a") as fd:
|
|
100
|
+
fd.write(line)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def obj_to_xml(obj: Any) -> str:
|
|
6
|
+
"""Return an XML representation of the given object (dict, list, or primitive).
|
|
7
|
+
Does NOT add a common root element if the given obj is a list.
|
|
8
|
+
Does NOT work for nested dict structures."""
|
|
9
|
+
if isinstance(obj, list):
|
|
10
|
+
return "".join([obj_to_xml(o) for o in obj])
|
|
11
|
+
if isinstance(obj, dict):
|
|
12
|
+
return "".join([f"<{k}>{obj_to_xml(v)}</{k}>" for (k, v) in obj.items()])
|
|
13
|
+
return str(obj)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def strip_xmlns(obj: Any) -> Any:
|
|
17
|
+
"""Strip xmlns attributes from a dict returned by xmltodict.parse."""
|
|
18
|
+
if isinstance(obj, list):
|
|
19
|
+
return [strip_xmlns(item) for item in obj]
|
|
20
|
+
if isinstance(obj, dict):
|
|
21
|
+
# Remove xmlns attribute.
|
|
22
|
+
obj.pop("@xmlns", None)
|
|
23
|
+
if len(obj) == 1 and "#text" in obj:
|
|
24
|
+
# If the only remaining key is the #text key, elide the dict
|
|
25
|
+
# entirely, to match the structure that xmltodict.parse would have
|
|
26
|
+
# returned if the xmlns namespace hadn't been present.
|
|
27
|
+
return obj["#text"]
|
|
28
|
+
return {k: strip_xmlns(v) for k, v in obj.items()}
|
|
29
|
+
return obj
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_valid_xml(xml_string: str) -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Check if the given string is a valid XML document.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
# Attempt to parse the XML string
|
|
38
|
+
ET.fromstring(xml_string.encode("utf-8"))
|
|
39
|
+
return True
|
|
40
|
+
except ET.ParseError:
|
|
41
|
+
return False
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '4.14.1.dev8'
|
|
32
|
+
__version_tuple__ = version_tuple = (4, 14, 1, 'dev8')
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|