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,326 @@
1
+ import logging
2
+ import math
3
+ import os
4
+ import re
5
+ from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse
6
+
7
+ import requests
8
+ from requests.models import CaseInsensitiveDict, Response
9
+
10
+ from localstack_cli import config
11
+
12
+ from .strings import to_str
13
+
14
+ # chunk size for file downloads
15
+ DOWNLOAD_CHUNK_SIZE = 1024 * 1024
16
+
17
+ ACCEPT = "accept"
18
+ LOG = logging.getLogger(__name__)
19
+
20
+
21
+ def uses_chunked_encoding(response):
22
+ return response.headers.get("Transfer-Encoding", "").lower() == "chunked"
23
+
24
+
25
+ def parse_chunked_data(data):
26
+ """Parse the body of an HTTP message transmitted with chunked transfer encoding."""
27
+ data = (data or "").strip()
28
+ chunks = []
29
+ while data:
30
+ length = re.match(r"^([0-9a-zA-Z]+)\r\n.*", data)
31
+ if not length:
32
+ break
33
+ length = length.group(1).lower()
34
+ length = int(length, 16)
35
+ data = data.partition("\r\n")[2]
36
+ chunks.append(data[:length])
37
+ data = data[length:].strip()
38
+ return "".join(chunks)
39
+
40
+
41
+ def create_chunked_data(data, chunk_size: int = 80):
42
+ dl = len(data)
43
+ ret = ""
44
+ for i in range(dl // chunk_size):
45
+ ret += f"{hex(chunk_size)[2:]}\r\n"
46
+ ret += f"{data[i * chunk_size : (i + 1) * chunk_size]}\r\n\r\n"
47
+
48
+ if len(data) % chunk_size != 0:
49
+ ret += f"{hex(len(data) % chunk_size)[2:]}\r\n"
50
+ ret += f"{data[-(len(data) % chunk_size) :]}\r\n"
51
+
52
+ ret += "0\r\n\r\n"
53
+ return ret
54
+
55
+
56
+ def canonicalize_headers(headers: dict | CaseInsensitiveDict) -> dict:
57
+ if not headers:
58
+ return headers
59
+
60
+ def _normalize(name):
61
+ if name.lower().startswith(ACCEPT):
62
+ return name.lower()
63
+ return name
64
+
65
+ result = {_normalize(k): v for k, v in headers.items()}
66
+ return result
67
+
68
+
69
+ def add_path_parameters_to_url(uri: str, path_params: list):
70
+ url = urlparse(uri)
71
+ last_character = (
72
+ "/" if (len(url.path) == 0 or url.path[-1] != "/") and len(path_params) > 0 else ""
73
+ )
74
+ new_path = url.path + last_character + "/".join(path_params)
75
+ return urlunparse(url._replace(path=new_path))
76
+
77
+
78
+ def add_query_params_to_url(uri: str, query_params: dict) -> str:
79
+ """
80
+ Add query parameters to the uri.
81
+ :param uri: the base uri it can contains path arguments and query parameters
82
+ :param query_params: new query parameters to be added
83
+ :return: the resulting URL
84
+ """
85
+
86
+ # parse the incoming uri
87
+ url = urlparse(uri)
88
+
89
+ # parses the query part, if exists, into a dict
90
+ query_dict = dict(parse_qsl(url.query))
91
+
92
+ # updates the dict with new query parameters
93
+ query_dict.update(query_params)
94
+
95
+ # encodes query parameters
96
+ url_query = urlencode(query_dict)
97
+
98
+ # replaces the existing query
99
+ url_parse = url._replace(query=url_query)
100
+
101
+ return urlunparse(url_parse)
102
+
103
+
104
+ def make_http_request(
105
+ url: str, data: bytes | str = None, headers: dict[str, str] = None, method: str = "GET"
106
+ ) -> Response:
107
+ return requests.request(
108
+ url=url, method=method, headers=headers, data=data, auth=NetrcBypassAuth(), verify=False
109
+ )
110
+
111
+
112
+ class NetrcBypassAuth(requests.auth.AuthBase):
113
+ def __call__(self, r):
114
+ return r
115
+
116
+
117
+ class _RequestsSafe:
118
+ """Wrapper around requests library, which can prevent it from verifying
119
+ SSL certificates or reading credentials from ~/.netrc file"""
120
+
121
+ verify_ssl = True
122
+
123
+ def __getattr__(self, name):
124
+ method = requests.__dict__.get(name.lower())
125
+ if not method:
126
+ return method
127
+
128
+ def _wrapper(*args, **kwargs):
129
+ if "auth" not in kwargs:
130
+ kwargs["auth"] = NetrcBypassAuth()
131
+ url = kwargs.get("url") or (args[1] if name == "request" else args[0])
132
+ if not self.verify_ssl and url.startswith("https://") and "verify" not in kwargs:
133
+ kwargs["verify"] = False
134
+ return method(*args, **kwargs)
135
+
136
+ return _wrapper
137
+
138
+
139
+ # create safe_requests instance
140
+ safe_requests = _RequestsSafe()
141
+
142
+
143
+ def parse_request_data(method: str, path: str, data=None, headers=None) -> dict:
144
+ """Extract request data either from query string as well as request body (e.g., for POST)."""
145
+ result = {}
146
+ headers = headers or {}
147
+ content_type = headers.get("Content-Type", "")
148
+
149
+ # add query params to result
150
+ parsed_path = urlparse(path)
151
+ result.update(parse_qs(parsed_path.query))
152
+
153
+ # add params from url-encoded payload
154
+ if method in ["POST", "PUT", "PATCH"] and (not content_type or "form-" in content_type):
155
+ # content-type could be either "application/x-www-form-urlencoded" or "multipart/form-data"
156
+ try:
157
+ params = parse_qs(to_str(data or ""))
158
+ result.update(params)
159
+ except Exception:
160
+ pass # probably binary / JSON / non-URL encoded payload - ignore
161
+
162
+ # select first elements from result lists (this is assuming we are not using parameter lists!)
163
+ result = {k: v[0] for k, v in result.items()}
164
+ return result
165
+
166
+
167
+ def get_proxies() -> dict[str, str]:
168
+ proxy_map = {}
169
+ if config.OUTBOUND_HTTP_PROXY:
170
+ proxy_map["http"] = config.OUTBOUND_HTTP_PROXY
171
+ if config.OUTBOUND_HTTPS_PROXY:
172
+ proxy_map["https"] = config.OUTBOUND_HTTPS_PROXY
173
+ return proxy_map
174
+
175
+
176
+ def download(
177
+ url: str,
178
+ path: str,
179
+ verify_ssl: bool = True,
180
+ timeout: float = None,
181
+ request_headers: dict | None = None,
182
+ quiet: bool = False,
183
+ ) -> None:
184
+ """Downloads file at url to the given path. Raises TimeoutError if the optional timeout (in secs) is reached.
185
+
186
+ If `quiet` is passed, do not log any status messages. Error messages are still logged.
187
+ """
188
+
189
+ # make sure we're creating a new session here to enable parallel file downloads
190
+ s = requests.Session()
191
+ proxies = get_proxies()
192
+ if proxies:
193
+ s.proxies.update(proxies)
194
+
195
+ # Use REQUESTS_CA_BUNDLE path. If it doesn't exist, use the method provided settings.
196
+ # Note that a value that is not False, will result to True and will get the bundle file.
197
+ _verify = os.getenv("REQUESTS_CA_BUNDLE", verify_ssl)
198
+
199
+ r = None
200
+ try:
201
+ r = s.get(url, stream=True, verify=_verify, timeout=timeout, headers=request_headers)
202
+ # check status code before attempting to read body
203
+ if not r.ok:
204
+ raise Exception(f"Failed to download {url}, response code {r.status_code}")
205
+
206
+ total_size = 0
207
+ if r.headers.get("Content-Length"):
208
+ total_size = int(r.headers.get("Content-Length"))
209
+
210
+ total_downloaded = 0
211
+ if not os.path.exists(os.path.dirname(path)):
212
+ os.makedirs(os.path.dirname(path))
213
+ if not quiet:
214
+ LOG.debug("Starting download from %s to %s", url, path)
215
+ with open(path, "wb") as f:
216
+ iter_length = 0
217
+ percentage_limit = next_percentage_record = 10 # print a log line for every 10%
218
+ iter_limit = (
219
+ 1000000 # if we can't tell the percentage, print a log line for every 1MB chunk
220
+ )
221
+ for chunk in r.iter_content(DOWNLOAD_CHUNK_SIZE):
222
+ # explicitly check the raw stream, since the size from the chunk can be bigger than the amount of
223
+ # bytes transferred over the wire due to transparent decompression (f.e. GZIP)
224
+ new_total_downloaded = r.raw.tell()
225
+ iter_length += new_total_downloaded - total_downloaded
226
+ total_downloaded = new_total_downloaded
227
+ if chunk: # filter out keep-alive new chunks
228
+ f.write(chunk)
229
+ elif not quiet:
230
+ LOG.debug(
231
+ "Empty chunk %s (total %dK of %dK) from %s",
232
+ chunk,
233
+ total_downloaded / 1024,
234
+ total_size / 1024,
235
+ url,
236
+ )
237
+
238
+ if total_size > 0 and (
239
+ (current_percent := total_downloaded / total_size * 100)
240
+ >= next_percentage_record
241
+ ):
242
+ # increment the limit for the next log output (ensure that there is max 1 log message per block)
243
+ # f.e. percentage_limit is 10, current percentage is 71: next log is earliest at 80%
244
+ next_percentage_record = (
245
+ math.floor(current_percent / percentage_limit) * percentage_limit
246
+ + percentage_limit
247
+ )
248
+ if not quiet:
249
+ LOG.debug(
250
+ "Downloaded %d%% (total %dK of %dK) to %s",
251
+ current_percent,
252
+ total_downloaded / 1024,
253
+ total_size / 1024,
254
+ path,
255
+ )
256
+ iter_length = 0
257
+ elif total_size <= 0 and iter_length >= iter_limit:
258
+ if not quiet:
259
+ # print log message every x K if the total size is not known
260
+ LOG.debug(
261
+ "Downloaded %dK (total %dK) to %s",
262
+ iter_length / 1024,
263
+ total_downloaded / 1024,
264
+ path,
265
+ )
266
+ iter_length = 0
267
+ f.flush()
268
+ os.fsync(f)
269
+ if os.path.getsize(path) == 0:
270
+ LOG.warning("Zero bytes downloaded from %s, retrying", url)
271
+ download(url, path, verify_ssl)
272
+ return
273
+ if not quiet:
274
+ LOG.debug(
275
+ "Done downloading %s, response code %s, total %dK",
276
+ url,
277
+ r.status_code,
278
+ total_downloaded / 1024,
279
+ )
280
+ except requests.exceptions.ReadTimeout as e:
281
+ raise TimeoutError(f"Timeout ({timeout}) reached on download: {url} - {e}")
282
+ finally:
283
+ if r is not None:
284
+ r.close()
285
+ s.close()
286
+
287
+
288
+ def download_github_artifact(url: str, target_file: str, timeout: int = None):
289
+ """Download file from main URL or fallback URL (to avoid firewall errors if github.com is blocked).
290
+ Optionally allows to define a timeout in seconds."""
291
+
292
+ def do_download(
293
+ download_url: str, request_headers: dict | None = None, print_error: bool = False
294
+ ):
295
+ try:
296
+ download(download_url, target_file, timeout=timeout, request_headers=request_headers)
297
+ return True
298
+ except Exception as e:
299
+ if print_error:
300
+ LOG.error(
301
+ "Unable to download Github artifact from %s to %s: %s %s",
302
+ url,
303
+ target_file,
304
+ e,
305
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
306
+ )
307
+
308
+ # if a GitHub API token is set, use it to avoid rate limiting issues
309
+ gh_token = os.environ.get("GITHUB_API_TOKEN")
310
+ gh_auth_headers = None
311
+ if gh_token:
312
+ gh_auth_headers = {"authorization": f"Bearer {gh_token}"}
313
+ result = do_download(url, request_headers=gh_auth_headers)
314
+ if not result:
315
+ # TODO: use regex below to allow different branch names than "master"
316
+ url = url.replace("https://github.com", "https://cdn.jsdelivr.net/gh")
317
+ # The URL structure is https://cdn.jsdelivr.net/gh/user/repo@branch/file.js
318
+ url = url.replace("/raw/master/", "@master/")
319
+ # Do not send the GitHub auth token to the CDN
320
+ do_download(url, print_error=True)
321
+
322
+
323
+ # TODO move to aws_responses.py?
324
+ def replace_response_content(response, pattern, replacement):
325
+ content = to_str(response.content or "")
326
+ response._content = re.sub(pattern, replacement, content)
@@ -0,0 +1,219 @@
1
+ import base64
2
+ import decimal
3
+ import json
4
+ import logging
5
+ import os
6
+ from datetime import date, datetime
7
+ from json import JSONDecodeError
8
+ from typing import Any
9
+
10
+ from localstack_cli.config import HostAndPort
11
+
12
+ from .numbers import is_number
13
+ from .strings import to_str
14
+ from .time import timestamp_millis
15
+
16
+ LOG = logging.getLogger(__name__)
17
+
18
+
19
+ class CustomEncoder(json.JSONEncoder):
20
+ """Helper class to convert JSON documents with datetime, decimals, or bytes."""
21
+
22
+ def default(self, o):
23
+ import yaml # leave import here, to avoid breaking our Lambda tests!
24
+
25
+ if isinstance(o, HostAndPort):
26
+ return str(o)
27
+ if isinstance(o, decimal.Decimal):
28
+ if o % 1 > 0:
29
+ return float(o)
30
+ else:
31
+ return int(o)
32
+ if isinstance(o, (datetime, date)):
33
+ return timestamp_millis(o)
34
+ if isinstance(o, yaml.ScalarNode):
35
+ if o.tag == "tag:yaml.org,2002:int":
36
+ return int(o.value)
37
+ if o.tag == "tag:yaml.org,2002:float":
38
+ return float(o.value)
39
+ if o.tag == "tag:yaml.org,2002:bool":
40
+ return bool(o.value)
41
+ return str(o.value)
42
+ try:
43
+ if isinstance(o, bytes):
44
+ return to_str(o)
45
+ return super().default(o)
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ class BytesEncoder(json.JSONEncoder):
51
+ """Specialized JSON encoder that encode bytes into Base64 strings."""
52
+
53
+ def default(self, obj):
54
+ if isinstance(obj, bytes):
55
+ return to_str(base64.b64encode(obj))
56
+ return super().default(obj)
57
+
58
+
59
+ class FileMappedDocument(dict):
60
+ """A dictionary that is mapped to a json document on disk.
61
+
62
+ When the document is created, an attempt is made to load existing contents from disk. To load changes from
63
+ concurrent writes, run load(). To save and overwrite the current document on disk, run save().
64
+ """
65
+
66
+ path: str | os.PathLike
67
+
68
+ def __init__(self, path: str | os.PathLike, mode=0o664):
69
+ super().__init__()
70
+ self.path = path
71
+ self.mode = mode
72
+ self.load()
73
+
74
+ def load(self):
75
+ if not os.path.exists(self.path):
76
+ return
77
+
78
+ if os.path.isdir(self.path):
79
+ raise IsADirectoryError
80
+
81
+ with open(self.path) as fd:
82
+ self.update(json.load(fd))
83
+
84
+ def save(self):
85
+ if os.path.isdir(self.path):
86
+ raise IsADirectoryError
87
+
88
+ if not os.path.exists(self.path):
89
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
90
+
91
+ def opener(path, flags):
92
+ _fd = os.open(path, flags, self.mode)
93
+ os.chmod(path, mode=self.mode, follow_symlinks=True)
94
+ return _fd
95
+
96
+ with open(self.path, "w", opener=opener) as fd:
97
+ json.dump(self, fd)
98
+
99
+
100
+ def clone(item):
101
+ return json.loads(json.dumps(item))
102
+
103
+
104
+ def clone_safe(item):
105
+ return clone(json_safe(item))
106
+
107
+
108
+ def parse_json_or_yaml(markup: str) -> Any:
109
+ import yaml # leave import here, to avoid breaking our Lambda tests!
110
+
111
+ try:
112
+ return json.loads(markup)
113
+ except Exception:
114
+ try:
115
+ return clone_safe(yaml.safe_load(markup))
116
+ except Exception:
117
+ try:
118
+ return clone_safe(yaml.load(markup, Loader=yaml.SafeLoader))
119
+ except Exception:
120
+ raise
121
+
122
+
123
+ def try_json(data: str):
124
+ """
125
+ Tries to deserialize the passed json input to an object if possible, otherwise returns the original input.
126
+ :param data: string
127
+ :return: deserialize version of input
128
+ """
129
+ try:
130
+ return json.loads(to_str(data or "{}"))
131
+ except JSONDecodeError:
132
+ LOG.warning("failed serialize to json, fallback to original")
133
+ return data
134
+
135
+
136
+ def json_safe(item: Any) -> Any:
137
+ """Return a copy of the given object (e.g., dict) that is safe for JSON dumping"""
138
+ try:
139
+ return json.loads(json.dumps(item, cls=CustomEncoder))
140
+ except Exception:
141
+ item = fix_json_keys(item)
142
+ return json.loads(json.dumps(item, cls=CustomEncoder))
143
+
144
+
145
+ def fix_json_keys(item: Any):
146
+ """make sure the keys of a JSON are strings (not binary type or other)"""
147
+ item_copy = item
148
+ if isinstance(item, list):
149
+ item_copy = []
150
+ for i in item:
151
+ item_copy.append(fix_json_keys(i))
152
+ if isinstance(item, dict):
153
+ item_copy = {}
154
+ for k, v in item.items():
155
+ item_copy[to_str(k)] = fix_json_keys(v)
156
+ return item_copy
157
+
158
+
159
+ def canonical_json(obj):
160
+ return json.dumps(obj, sort_keys=True)
161
+
162
+
163
+ def extract_jsonpath(value, path):
164
+ from jsonpath_rw import parse
165
+
166
+ jsonpath_expr = parse(path)
167
+ result = [match.value for match in jsonpath_expr.find(value)]
168
+ result = result[0] if len(result) == 1 else result
169
+ return result
170
+
171
+
172
+ def assign_to_path(target: dict, path: str, value: any, delimiter: str = ".") -> dict:
173
+ """Assign the given value to a dict. If the path doesn't exist in the target dict, it will be created.
174
+ The delimiter can be used to provide a path with a different delimiter.
175
+
176
+ Examples:
177
+ - assign_to_path({}, "a", "b") => {"a": "b"}
178
+ - assign_to_path({}, "a.b.c", "d") => {"a": {"b": {"c": "d"}}}
179
+ - assign_to_path({}, "a.b/c", "d", delimiter="/") => {"a.b": {"c": "d"}}
180
+
181
+ """
182
+ parts = path.strip(delimiter).split(delimiter)
183
+
184
+ if len(parts) == 1:
185
+ target[parts[0]] = value
186
+ return target
187
+
188
+ path_to_parent = delimiter.join(parts[:-1])
189
+ parent = extract_from_jsonpointer_path(target, path_to_parent, delimiter, auto_create=True)
190
+ if not isinstance(parent, dict):
191
+ LOG.debug(
192
+ 'Unable to find parent (type %s) for path "%s" in object: %s',
193
+ type(parent),
194
+ path,
195
+ target,
196
+ )
197
+ return
198
+ path_end = int(parts[-1]) if is_number(parts[-1]) else parts[-1].replace("~1", "/")
199
+ parent[path_end] = value
200
+ return target
201
+
202
+
203
+ def extract_from_jsonpointer_path(target, path: str, delimiter: str = "/", auto_create=False):
204
+ parts = path.strip(delimiter).split(delimiter)
205
+ for part in parts:
206
+ path_part = int(part) if is_number(part) else part
207
+ if isinstance(target, list) and not is_number(path_part):
208
+ if path_part == "-":
209
+ # special case where path is like /path/to/list/- where "/-" means "append to list"
210
+ continue
211
+ LOG.warning('Attempting to extract non-int index "%s" from list: %s', path_part, target)
212
+ return None
213
+ target_new = target[path_part] if isinstance(target, list) else target.get(path_part)
214
+ if target_new is None:
215
+ if not auto_create:
216
+ return
217
+ target[path_part] = target_new = {}
218
+ target = target_new
219
+ return target