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,235 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import re
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from .collections import ensure_list
|
|
8
|
+
from .strings import first_char_to_lower, first_char_to_upper
|
|
9
|
+
|
|
10
|
+
ComplexType = list | dict | object
|
|
11
|
+
|
|
12
|
+
_T = TypeVar("_T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Value(Generic[_T]):
|
|
16
|
+
"""
|
|
17
|
+
Simple value container.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
value: _T | None
|
|
21
|
+
|
|
22
|
+
def __init__(self, value: _T = None) -> None:
|
|
23
|
+
self.value = value
|
|
24
|
+
|
|
25
|
+
def clear(self):
|
|
26
|
+
self.value = None
|
|
27
|
+
|
|
28
|
+
def set(self, value: _T):
|
|
29
|
+
self.value = value
|
|
30
|
+
|
|
31
|
+
def is_set(self) -> bool:
|
|
32
|
+
return self.value is not None
|
|
33
|
+
|
|
34
|
+
def get(self) -> _T | None:
|
|
35
|
+
return self.value
|
|
36
|
+
|
|
37
|
+
def __bool__(self):
|
|
38
|
+
return True if self.value else False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ArbitraryAccessObj:
|
|
42
|
+
"""Dummy object that can be arbitrarily accessed - any attributes, as a callable, item assignment, ..."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, name=None):
|
|
45
|
+
self.name = name
|
|
46
|
+
|
|
47
|
+
def __getattr__(self, name, *args, **kwargs):
|
|
48
|
+
return ArbitraryAccessObj(name)
|
|
49
|
+
|
|
50
|
+
def __call__(self, *args, **kwargs):
|
|
51
|
+
if self.name in ["items", "keys", "values"] and not args and not kwargs:
|
|
52
|
+
return []
|
|
53
|
+
return ArbitraryAccessObj()
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, *args, **kwargs):
|
|
56
|
+
return ArbitraryAccessObj()
|
|
57
|
+
|
|
58
|
+
def __setitem__(self, *args, **kwargs):
|
|
59
|
+
return ArbitraryAccessObj()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Mock:
|
|
63
|
+
"""Dummy class that can be used for mocking custom attributes."""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ObjectIdHashComparator:
|
|
69
|
+
"""Simple wrapper class that allows us to create a hashset using the object id(..) as the entries' hash value"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, obj):
|
|
72
|
+
self.obj = obj
|
|
73
|
+
self._hash = id(obj)
|
|
74
|
+
|
|
75
|
+
def __hash__(self):
|
|
76
|
+
return self._hash
|
|
77
|
+
|
|
78
|
+
def __eq__(self, other):
|
|
79
|
+
# assumption here is that we're comparing only against ObjectIdHash instances!
|
|
80
|
+
return self.obj == other.obj
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SubtypesInstanceManager:
|
|
84
|
+
"""Simple instance manager base class that scans the subclasses of a base type for concrete named
|
|
85
|
+
implementations, and lazily creates and returns (singleton) instances on demand."""
|
|
86
|
+
|
|
87
|
+
_instances: dict[str, "SubtypesInstanceManager"]
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def get(cls, subtype_name: str, raise_if_missing: bool = True):
|
|
91
|
+
instances = cls.instances()
|
|
92
|
+
base_type = cls.get_base_type()
|
|
93
|
+
instance = instances.get(subtype_name)
|
|
94
|
+
if instance is None:
|
|
95
|
+
# lazily load subtype instance (required if new plugins are dynamically loaded at runtime)
|
|
96
|
+
for clazz in get_all_subclasses(base_type):
|
|
97
|
+
impl_name = clazz.impl_name()
|
|
98
|
+
if impl_name not in instances and subtype_name == impl_name:
|
|
99
|
+
instances[impl_name] = clazz()
|
|
100
|
+
instance = instances.get(subtype_name)
|
|
101
|
+
if not instance and raise_if_missing:
|
|
102
|
+
raise NotImplementedError(
|
|
103
|
+
f"Unable to find implementation named '{subtype_name}' for base type {base_type}"
|
|
104
|
+
)
|
|
105
|
+
return instance
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def instances(cls) -> dict[str, "SubtypesInstanceManager"]:
|
|
109
|
+
base_type = cls.get_base_type()
|
|
110
|
+
if not hasattr(base_type, "_instances"):
|
|
111
|
+
base_type._instances = {}
|
|
112
|
+
return base_type._instances
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def impl_name() -> str:
|
|
116
|
+
"""Name of this concrete subtype - to be implemented by subclasses."""
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_base_type(cls) -> type:
|
|
121
|
+
"""Get the base class for which instances are being managed - can be customized by subtypes."""
|
|
122
|
+
return cls
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# this requires that all subclasses have been imported before(!)
|
|
126
|
+
def get_all_subclasses(clazz: type) -> set[type]:
|
|
127
|
+
"""Recursively get all subclasses of the given class."""
|
|
128
|
+
result = set()
|
|
129
|
+
subs = clazz.__subclasses__()
|
|
130
|
+
for sub in subs:
|
|
131
|
+
result.add(sub)
|
|
132
|
+
result.update(get_all_subclasses(sub))
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def fully_qualified_class_name(klass: type) -> str:
|
|
137
|
+
return f"{klass.__module__}.{klass.__name__}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def not_none_or(value: Any | None, alternative: Any) -> Any:
|
|
141
|
+
"""Return 'value' if it is not None, or 'alternative' otherwise."""
|
|
142
|
+
return value if value is not None else alternative
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def recurse_object(obj: ComplexType, func: Callable, path: str = "") -> ComplexType:
|
|
146
|
+
"""Recursively apply `func` to `obj` (might be a list, dict, or other object)."""
|
|
147
|
+
obj = func(obj, path=path)
|
|
148
|
+
if isinstance(obj, list):
|
|
149
|
+
for i in range(len(obj)):
|
|
150
|
+
tmp_path = f"{path or '.'}[{i}]"
|
|
151
|
+
obj[i] = recurse_object(obj[i], func, tmp_path)
|
|
152
|
+
elif isinstance(obj, dict):
|
|
153
|
+
for k, v in obj.items():
|
|
154
|
+
tmp_path = f"{f'{path}.' if path else ''}{k}"
|
|
155
|
+
obj[k] = recurse_object(v, func, tmp_path)
|
|
156
|
+
return obj
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def keys_to(
|
|
160
|
+
obj: ComplexType, op: Callable[[str], str], skip_children_of: list[str] = None
|
|
161
|
+
) -> ComplexType:
|
|
162
|
+
"""Recursively changes all dict keys to apply op. Skip children
|
|
163
|
+
of any elements whose names are contained in skip_children_of (e.g., ['Tags'])"""
|
|
164
|
+
skip_children_of = ensure_list(skip_children_of or [])
|
|
165
|
+
|
|
166
|
+
def fix_keys(o, path="", **kwargs):
|
|
167
|
+
if any(re.match(rf"(^|.*\.){k}($|[.\[].*)", path) for k in skip_children_of):
|
|
168
|
+
return o
|
|
169
|
+
if isinstance(o, dict):
|
|
170
|
+
for k, v in dict(o).items():
|
|
171
|
+
o.pop(k)
|
|
172
|
+
o[op(k)] = v
|
|
173
|
+
return o
|
|
174
|
+
|
|
175
|
+
result = recurse_object(obj, fix_keys)
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def keys_to_lower(obj: ComplexType, skip_children_of: list[str] = None) -> ComplexType:
|
|
180
|
+
return keys_to(obj, first_char_to_lower, skip_children_of)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def keys_to_upper(obj: ComplexType, skip_children_of: list[str] = None) -> ComplexType:
|
|
184
|
+
return keys_to(obj, first_char_to_upper, skip_children_of)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def singleton_factory(factory: Callable[[], _T]) -> Callable[[], _T]:
|
|
188
|
+
"""
|
|
189
|
+
Decorator for methods that create a particular value once and then return the same value in a thread safe way.
|
|
190
|
+
|
|
191
|
+
:param factory: the method to decorate
|
|
192
|
+
:return: a threadsafe singleton factory
|
|
193
|
+
"""
|
|
194
|
+
lock = threading.RLock()
|
|
195
|
+
instance: Value[_T] = Value()
|
|
196
|
+
|
|
197
|
+
@functools.wraps(factory)
|
|
198
|
+
def _singleton_factory() -> _T:
|
|
199
|
+
if instance.is_set():
|
|
200
|
+
return instance.get()
|
|
201
|
+
|
|
202
|
+
with lock:
|
|
203
|
+
if not instance:
|
|
204
|
+
instance.set(factory())
|
|
205
|
+
|
|
206
|
+
return instance.get()
|
|
207
|
+
|
|
208
|
+
_singleton_factory.clear = instance.clear
|
|
209
|
+
|
|
210
|
+
return _singleton_factory
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_value_from_path(data, path):
|
|
214
|
+
keys = path.split(".")
|
|
215
|
+
try:
|
|
216
|
+
result = functools.reduce(dict.__getitem__, keys, data)
|
|
217
|
+
return result
|
|
218
|
+
except KeyError:
|
|
219
|
+
# Handle the case where the path is not valid for the given dictionary
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def set_value_at_path(data, path, new_value):
|
|
224
|
+
keys = path.split(".")
|
|
225
|
+
last_key = keys[-1]
|
|
226
|
+
|
|
227
|
+
# Traverse the dictionary to the second-to-last level
|
|
228
|
+
nested_dict = functools.reduce(dict.__getitem__, keys[:-1], data)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Set the new value
|
|
232
|
+
nested_dict[last_key] = new_value
|
|
233
|
+
except KeyError:
|
|
234
|
+
# Handle the case where the path is not valid for the given dictionary
|
|
235
|
+
raise ValueError(f"Invalid path: {path}")
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import types
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_defining_object(method):
|
|
9
|
+
"""Returns either the class or the module that defines the given function/method."""
|
|
10
|
+
# adapted from https://stackoverflow.com/a/25959545/804840
|
|
11
|
+
if inspect.ismethod(method):
|
|
12
|
+
return method.__self__
|
|
13
|
+
|
|
14
|
+
if inspect.isfunction(method):
|
|
15
|
+
class_name = method.__qualname__.split(".<locals>", 1)[0].rsplit(".", 1)[0]
|
|
16
|
+
try:
|
|
17
|
+
# method is not bound but referenced by a class, like MyClass.mymethod
|
|
18
|
+
cls = getattr(inspect.getmodule(method), class_name)
|
|
19
|
+
except AttributeError:
|
|
20
|
+
cls = method.__globals__.get(class_name)
|
|
21
|
+
|
|
22
|
+
if isinstance(cls, type):
|
|
23
|
+
return cls
|
|
24
|
+
|
|
25
|
+
# method is a module-level function
|
|
26
|
+
return inspect.getmodule(method)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def to_metadata_string(obj: Any) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Creates a string that helps humans understand where the given object comes from. Examples::
|
|
32
|
+
|
|
33
|
+
to_metadata_string(func_thread.run) == "function(localstack.utils.threads:FuncThread.run)"
|
|
34
|
+
|
|
35
|
+
:param obj: a class, module, method, function or object
|
|
36
|
+
:return: a string representing the objects origin
|
|
37
|
+
"""
|
|
38
|
+
if inspect.isclass(obj):
|
|
39
|
+
return f"class({obj.__module__}:{obj.__name__})"
|
|
40
|
+
if inspect.ismodule(obj):
|
|
41
|
+
return f"module({obj.__name__})"
|
|
42
|
+
if inspect.ismethod(obj):
|
|
43
|
+
return f"method({obj.__module__}:{obj.__qualname__})"
|
|
44
|
+
if inspect.isfunction(obj):
|
|
45
|
+
# TODO: distinguish bound method
|
|
46
|
+
return f"function({obj.__module__}:{obj.__qualname__})"
|
|
47
|
+
if isinstance(obj, object):
|
|
48
|
+
return f"object({obj.__module__}:{obj.__class__.__name__})"
|
|
49
|
+
return str(obj)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_patch_proxy(target: Callable, new: Callable):
|
|
53
|
+
"""
|
|
54
|
+
Creates a proxy that calls `new` but passes as first argument the target.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@functools.wraps(target)
|
|
58
|
+
def proxy(*args, **kwargs):
|
|
59
|
+
if _is_bound_method:
|
|
60
|
+
# bound object "self" is passed as first argument if this is a bound method
|
|
61
|
+
args = args[1:]
|
|
62
|
+
return new(target, *args, **kwargs)
|
|
63
|
+
|
|
64
|
+
# keep track of the real proxy subject (i.e., the new function that is used as patch)
|
|
65
|
+
proxy.__subject__ = new
|
|
66
|
+
|
|
67
|
+
_is_bound_method = inspect.ismethod(target)
|
|
68
|
+
if _is_bound_method:
|
|
69
|
+
proxy = types.MethodType(proxy, target.__self__)
|
|
70
|
+
|
|
71
|
+
return proxy
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Patch:
|
|
75
|
+
applied_patches: list["Patch"] = []
|
|
76
|
+
"""Bookkeeping for patches that are applied. You can use this to debug patches. For instance,
|
|
77
|
+
you could write something like::
|
|
78
|
+
|
|
79
|
+
for patch in Patch.applied_patches:
|
|
80
|
+
print(patch)
|
|
81
|
+
|
|
82
|
+
Which will output in a human readable format information about the currently active patches.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
obj: Any
|
|
86
|
+
name: str
|
|
87
|
+
new: Any
|
|
88
|
+
|
|
89
|
+
def __init__(self, obj: Any, name: str, new: Any) -> None:
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.obj = obj
|
|
92
|
+
self.name = name
|
|
93
|
+
try:
|
|
94
|
+
self.old = getattr(self.obj, name)
|
|
95
|
+
except AttributeError:
|
|
96
|
+
self.old = None
|
|
97
|
+
self.new = new
|
|
98
|
+
self.is_applied = False
|
|
99
|
+
|
|
100
|
+
def apply(self):
|
|
101
|
+
if self.is_applied:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if self.old and self.name == "__getattr__":
|
|
105
|
+
raise Exception("You can't patch class types implementing __getattr__")
|
|
106
|
+
if not self.old and self.name != "__getattr__":
|
|
107
|
+
raise AttributeError(f"`{self.obj.__name__}` object has no attribute `{self.name}`")
|
|
108
|
+
setattr(self.obj, self.name, self.new)
|
|
109
|
+
Patch.applied_patches.append(self)
|
|
110
|
+
self.is_applied = True
|
|
111
|
+
|
|
112
|
+
def undo(self):
|
|
113
|
+
if not self.is_applied:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# If we added a method to a class type, we don't have a self.old. We just delete __getattr__
|
|
117
|
+
setattr(self.obj, self.name, self.old) if self.old else delattr(self.obj, self.name)
|
|
118
|
+
Patch.applied_patches.remove(self)
|
|
119
|
+
self.is_applied = False
|
|
120
|
+
|
|
121
|
+
def __enter__(self):
|
|
122
|
+
self.apply()
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
126
|
+
self.undo()
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def extend_class(target: type, fn: Callable):
|
|
131
|
+
def _getattr(obj, name):
|
|
132
|
+
if name != fn.__name__:
|
|
133
|
+
raise AttributeError(f"`{target.__name__}` object has no attribute `{name}`")
|
|
134
|
+
|
|
135
|
+
return functools.partial(fn, obj)
|
|
136
|
+
|
|
137
|
+
return Patch(target, "__getattr__", _getattr)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def function(target: Callable, fn: Callable, pass_target: bool = True):
|
|
141
|
+
obj = get_defining_object(target)
|
|
142
|
+
name = target.__name__
|
|
143
|
+
|
|
144
|
+
is_class_instance = not inspect.isclass(obj) and not inspect.ismodule(obj)
|
|
145
|
+
if is_class_instance:
|
|
146
|
+
# special case: If the defining object is not a class, but a class instance,
|
|
147
|
+
# then we need to bind the patch function to the target object. Also, we need
|
|
148
|
+
# to ensure that the final patched method has the same name as the original
|
|
149
|
+
# method on the defining object (required for restoring objects with patched
|
|
150
|
+
# methods from persistence, to avoid AttributeError).
|
|
151
|
+
fn.__name__ = name
|
|
152
|
+
fn = types.MethodType(fn, obj)
|
|
153
|
+
|
|
154
|
+
if pass_target:
|
|
155
|
+
new = create_patch_proxy(target, fn)
|
|
156
|
+
else:
|
|
157
|
+
new = fn
|
|
158
|
+
|
|
159
|
+
return Patch(obj, name, new)
|
|
160
|
+
|
|
161
|
+
def __str__(self):
|
|
162
|
+
try:
|
|
163
|
+
# try to unwrap the original underlying function that is used as patch (basically undoes what
|
|
164
|
+
# ``create_patch_proxy`` does)
|
|
165
|
+
new = self.new.__subject__
|
|
166
|
+
except AttributeError:
|
|
167
|
+
new = self.new
|
|
168
|
+
|
|
169
|
+
old = self.old
|
|
170
|
+
return f"Patch({to_metadata_string(old)} -> {to_metadata_string(new)}, applied={self.is_applied})"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Patches:
|
|
174
|
+
patches: list[Patch]
|
|
175
|
+
|
|
176
|
+
def __init__(self, patches: list[Patch] = None) -> None:
|
|
177
|
+
super().__init__()
|
|
178
|
+
|
|
179
|
+
self.patches = []
|
|
180
|
+
if patches:
|
|
181
|
+
self.patches.extend(patches)
|
|
182
|
+
|
|
183
|
+
def apply(self):
|
|
184
|
+
for p in self.patches:
|
|
185
|
+
p.apply()
|
|
186
|
+
|
|
187
|
+
def undo(self):
|
|
188
|
+
for p in self.patches:
|
|
189
|
+
p.undo()
|
|
190
|
+
|
|
191
|
+
def __enter__(self):
|
|
192
|
+
self.apply()
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
+
self.undo()
|
|
197
|
+
|
|
198
|
+
def add(self, patch: Patch):
|
|
199
|
+
self.patches.append(patch)
|
|
200
|
+
|
|
201
|
+
def function(self, target: Callable, fn: Callable, pass_target: bool = True):
|
|
202
|
+
self.add(Patch.function(target, fn, pass_target))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def patch(target, pass_target=True):
|
|
206
|
+
"""
|
|
207
|
+
Function decorator to create a patch via Patch.function and immediately apply it.
|
|
208
|
+
|
|
209
|
+
Example::
|
|
210
|
+
|
|
211
|
+
def echo(string):
|
|
212
|
+
return "echo " + string
|
|
213
|
+
|
|
214
|
+
@patch(target=echo)
|
|
215
|
+
def echo_uppercase(target, string):
|
|
216
|
+
return target(string).upper()
|
|
217
|
+
|
|
218
|
+
echo("foo")
|
|
219
|
+
# will print "ECHO FOO"
|
|
220
|
+
|
|
221
|
+
echo_uppercase.patch.undo()
|
|
222
|
+
echo("foo")
|
|
223
|
+
# will print "echo foo"
|
|
224
|
+
|
|
225
|
+
When you are patching classes, with ``pass_target=True``, the unbound function will be passed as the first
|
|
226
|
+
argument before ``self``.
|
|
227
|
+
|
|
228
|
+
For example::
|
|
229
|
+
|
|
230
|
+
@patch(target=MyEchoer.do_echo, pass_target=True)
|
|
231
|
+
def my_patch(fn, self, *args):
|
|
232
|
+
return fn(self, *args)
|
|
233
|
+
|
|
234
|
+
@patch(target=MyEchoer.do_echo, pass_target=False)
|
|
235
|
+
def my_patch(self, *args):
|
|
236
|
+
...
|
|
237
|
+
|
|
238
|
+
This decorator can also patch a class type with a new method.
|
|
239
|
+
|
|
240
|
+
For example:
|
|
241
|
+
@patch(target=MyEchoer)
|
|
242
|
+
def new_echo(self, *args):
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
:param target: the function or method to patch
|
|
246
|
+
:param pass_target: whether to pass the target to the patching function as first parameter
|
|
247
|
+
:returns: the same function, but with a patch created
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
@functools.wraps(target)
|
|
251
|
+
def wrapper(fn):
|
|
252
|
+
fn.patch = (
|
|
253
|
+
Patch.extend_class(target, fn)
|
|
254
|
+
if inspect.isclass(target)
|
|
255
|
+
else Patch.function(target, fn, pass_target=pass_target)
|
|
256
|
+
)
|
|
257
|
+
fn.patch.apply()
|
|
258
|
+
return fn
|
|
259
|
+
|
|
260
|
+
return wrapper
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def is_mac_os() -> bool:
|
|
6
|
+
return "darwin" == platform.system().lower()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_linux() -> bool:
|
|
10
|
+
return "linux" == platform.system().lower()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_windows() -> bool:
|
|
14
|
+
return "windows" == platform.system().lower()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache
|
|
18
|
+
def is_debian() -> bool:
|
|
19
|
+
from localstack_cli.utils.files import load_file
|
|
20
|
+
|
|
21
|
+
return "Debian" in load_file("/etc/issue", "")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache
|
|
25
|
+
def is_redhat() -> bool:
|
|
26
|
+
from localstack_cli.utils.files import load_file
|
|
27
|
+
|
|
28
|
+
return "rhel" in load_file("/etc/os-release", "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Arch(str):
|
|
32
|
+
"""LocalStack standardised machine architecture names"""
|
|
33
|
+
|
|
34
|
+
amd64 = "amd64"
|
|
35
|
+
arm64 = "arm64"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def standardized_arch(arch: str):
|
|
39
|
+
"""
|
|
40
|
+
Returns LocalStack standardised machine architecture name.
|
|
41
|
+
"""
|
|
42
|
+
if arch == "x86_64":
|
|
43
|
+
return Arch.amd64
|
|
44
|
+
if arch == "aarch64":
|
|
45
|
+
return Arch.arm64
|
|
46
|
+
return arch
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_arch() -> str:
|
|
50
|
+
"""
|
|
51
|
+
Returns the current machine architecture.
|
|
52
|
+
"""
|
|
53
|
+
arch = platform.machine()
|
|
54
|
+
return standardized_arch(arch)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# TODO: implement proper architecture detection (e.g., test whether an architecture-specific binary actually runs)
|
|
58
|
+
# because this naive implementation does not cover cross-architecture emulation
|
|
59
|
+
def is_arm_compatible() -> bool:
|
|
60
|
+
"""Returns true if the current machine is compatible with ARM instructions and false otherwise."""
|
|
61
|
+
return get_arch() == Arch.arm64
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_os() -> str:
|
|
65
|
+
if is_mac_os():
|
|
66
|
+
return "osx"
|
|
67
|
+
if is_linux():
|
|
68
|
+
return "linux"
|
|
69
|
+
if is_windows():
|
|
70
|
+
return "windows"
|
|
71
|
+
raise ValueError("Unable to determine local operating system")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def in_docker() -> bool:
|
|
75
|
+
from localstack_cli import config
|
|
76
|
+
|
|
77
|
+
return config.in_docker()
|