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,516 @@
1
+ import logging
2
+ import random
3
+ import re
4
+ import socket
5
+ import threading
6
+ from collections.abc import MutableMapping
7
+ from contextlib import closing
8
+ from typing import Any, NamedTuple
9
+ from urllib.parse import urlparse
10
+
11
+ import dns.resolver
12
+ from dnslib import DNSRecord
13
+
14
+ from localstack_cli import config, constants
15
+
16
+ from .collections import CustomExpiryTTLCache
17
+ from .numbers import is_number
18
+ from .objects import singleton_factory
19
+ from .sync import retry
20
+
21
+ LOG = logging.getLogger(__name__)
22
+
23
+ # regular expression for IPv4 addresses
24
+ IP_REGEX = (
25
+ r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
26
+ )
27
+
28
+ # many linux kernels use 32768-60999, RFC 6335 is 49152-65535, so we use a mix here
29
+ DYNAMIC_PORT_RANGE_START = 32768
30
+ DYNAMIC_PORT_RANGE_END = 65536
31
+
32
+ DEFAULT_PORT_RESERVED_SECONDS = 6
33
+ """Default nuber of seconds a port is reserved in a PortRange."""
34
+
35
+
36
+ class Port(NamedTuple):
37
+ """Represents a network port, with port number and protocol (TCP/UDP)"""
38
+
39
+ port: int
40
+ """the port number"""
41
+ protocol: str
42
+ """network protocol name (usually 'tcp' or 'udp')"""
43
+
44
+ @classmethod
45
+ def wrap(cls, port: "IntOrPort") -> "Port":
46
+ """Return the given port as a Port object, using 'tcp' as the default protocol."""
47
+ if isinstance(port, Port):
48
+ return port
49
+ return Port(port=port, protocol="tcp")
50
+
51
+
52
+ # simple helper type to encapsulate int/Port argument types
53
+ IntOrPort = int | Port
54
+
55
+
56
+ def is_port_open(
57
+ port_or_url: int | str,
58
+ http_path: str = None,
59
+ expect_success: bool = True,
60
+ protocols: str | list[str] | None = None,
61
+ quiet: bool = True,
62
+ ):
63
+ from localstack_cli.utils.http import safe_requests
64
+
65
+ protocols = protocols or ["tcp"]
66
+ port = port_or_url
67
+ if is_number(port):
68
+ port = int(port)
69
+ host = "localhost"
70
+ protocol = "http"
71
+ protocols = protocols if isinstance(protocols, list) else [protocols]
72
+ if isinstance(port, str):
73
+ url = urlparse(port_or_url)
74
+ port = url.port
75
+ host = url.hostname
76
+ protocol = url.scheme
77
+ nw_protocols = []
78
+ nw_protocols += [socket.SOCK_STREAM] if "tcp" in protocols else []
79
+ nw_protocols += [socket.SOCK_DGRAM] if "udp" in protocols else []
80
+ for nw_protocol in nw_protocols:
81
+ with closing(
82
+ socket.socket(socket.AF_INET if ":" not in host else socket.AF_INET6, nw_protocol)
83
+ ) as sock:
84
+ sock.settimeout(1)
85
+ if nw_protocol == socket.SOCK_DGRAM:
86
+ try:
87
+ if port == 53:
88
+ dnshost = "127.0.0.1" if host == "localhost" else host
89
+ resolver = dns.resolver.Resolver()
90
+ resolver.nameservers = [dnshost]
91
+ resolver.timeout = 1
92
+ resolver.lifetime = 1
93
+ answers = resolver.query("google.com", "A")
94
+ assert len(answers) > 0
95
+ else:
96
+ sock.sendto(b"", (host, port))
97
+ sock.recvfrom(1024)
98
+ except Exception:
99
+ if not quiet:
100
+ LOG.error(
101
+ "Error connecting to UDP port %s:%s",
102
+ host,
103
+ port,
104
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
105
+ )
106
+ return False
107
+ elif nw_protocol == socket.SOCK_STREAM:
108
+ result = sock.connect_ex((host, port))
109
+ if result != 0:
110
+ if not quiet:
111
+ LOG.warning(
112
+ "Error connecting to TCP port %s:%s (result=%s)", host, port, result
113
+ )
114
+ return False
115
+ if "tcp" not in protocols or not http_path:
116
+ return True
117
+ host = f"[{host}]" if ":" in host else host
118
+ url = f"{protocol}://{host}:{port}{http_path}"
119
+ try:
120
+ response = safe_requests.get(url, verify=False)
121
+ return not expect_success or response.status_code < 400
122
+ except Exception:
123
+ return False
124
+
125
+
126
+ def wait_for_port_open(
127
+ port: int, http_path: str = None, expect_success=True, retries=10, sleep_time=0.5
128
+ ):
129
+ """Ping the given TCP network port until it becomes available (for a given number of retries).
130
+ If 'http_path' is set, make a GET request to this path and assert a non-error response."""
131
+ return wait_for_port_status(
132
+ port,
133
+ http_path=http_path,
134
+ expect_success=expect_success,
135
+ retries=retries,
136
+ sleep_time=sleep_time,
137
+ )
138
+
139
+
140
+ def wait_for_port_closed(
141
+ port: int, http_path: str = None, expect_success=True, retries=10, sleep_time=0.5
142
+ ):
143
+ return wait_for_port_status(
144
+ port,
145
+ http_path=http_path,
146
+ expect_success=expect_success,
147
+ retries=retries,
148
+ sleep_time=sleep_time,
149
+ expect_closed=True,
150
+ )
151
+
152
+
153
+ def wait_for_port_status(
154
+ port: int,
155
+ http_path: str = None,
156
+ expect_success=True,
157
+ retries=10,
158
+ sleep_time=0.5,
159
+ expect_closed=False,
160
+ ):
161
+ """Ping the given TCP network port until it becomes (un)available (for a given number of retries)."""
162
+
163
+ def check():
164
+ status = is_port_open(port, http_path=http_path, expect_success=expect_success)
165
+ if bool(status) != (not expect_closed):
166
+ raise Exception(
167
+ "Port {} (path: {}) was not {}".format(
168
+ port, http_path, "closed" if expect_closed else "open"
169
+ )
170
+ )
171
+
172
+ return retry(check, sleep=sleep_time, retries=retries)
173
+
174
+
175
+ def port_can_be_bound(port: IntOrPort, address: str = "") -> bool:
176
+ """
177
+ Return whether a local port (TCP or UDP) can be bound to. Note that this is a stricter check
178
+ than is_port_open(...) above, as is_port_open() may return False if the port is
179
+ not accessible (i.e., does not respond), yet cannot be bound to.
180
+ """
181
+ try:
182
+ port = Port.wrap(port)
183
+ if port.protocol == "tcp":
184
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185
+ elif port.protocol == "udp":
186
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
187
+ else:
188
+ LOG.debug("Unsupported network protocol '%s' for port check", port.protocol)
189
+ return False
190
+ sock.bind((address, port.port))
191
+ return True
192
+ except OSError:
193
+ # either the port is used or we don't have permission to bind it
194
+ return False
195
+ except Exception:
196
+ LOG.error("cannot bind port %s", port, exc_info=LOG.isEnabledFor(logging.DEBUG))
197
+ return False
198
+
199
+
200
+ def get_free_udp_port(blocklist: list[int] = None) -> int:
201
+ blocklist = blocklist or []
202
+ for i in range(10):
203
+ udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
204
+ udp.bind(("", 0))
205
+ addr, port = udp.getsockname()
206
+ udp.close()
207
+ if port not in blocklist:
208
+ return port
209
+ raise Exception(f"Unable to determine free UDP port with blocklist {blocklist}")
210
+
211
+
212
+ def get_free_tcp_port(blocklist: list[int] = None) -> int:
213
+ """
214
+ Tries to bind a socket to port 0 and returns the port that was assigned by the system. If the port is
215
+ in the given ``blocklist``, or the port is marked as reserved in ``dynamic_port_range``, the procedure
216
+ is repeated for up to 50 times.
217
+
218
+ :param blocklist: an optional list of ports that are not allowed as random ports
219
+ :return: a free TCP port
220
+ """
221
+ blocklist = blocklist or []
222
+ for i in range(50):
223
+ tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
224
+ tcp.bind(("", 0))
225
+ addr, port = tcp.getsockname()
226
+ tcp.close()
227
+ if port not in blocklist and not dynamic_port_range.is_port_reserved(port):
228
+ try:
229
+ dynamic_port_range.mark_reserved(port)
230
+ except ValueError:
231
+ # depending on the ephemeral port range of the system, the allocated port may be outside what
232
+ # we defined as dynamic port range
233
+ pass
234
+ return port
235
+ raise Exception(f"Unable to determine free TCP port with blocklist {blocklist}")
236
+
237
+
238
+ def get_free_tcp_port_range(num_ports: int, max_attempts: int = 50) -> "PortRange":
239
+ """
240
+ Attempts to get a contiguous range of free ports from the dynamic port range. For instance,
241
+ ``get_free_tcp_port_range(4)`` may return the following result: ``PortRange(44000:44004)``.
242
+
243
+ :param num_ports: the number of ports in the range
244
+ :param max_attempts: the number of times to retry if a contiguous range was not found
245
+ :return: a port range of free TCP ports
246
+ :raises PortNotAvailableException: if max_attempts was reached to re-try
247
+ """
248
+ if num_ports < 2:
249
+ raise ValueError(f"invalid number of ports {num_ports}")
250
+
251
+ def _is_port_range_free(_range: PortRange):
252
+ for _port in _range:
253
+ if dynamic_port_range.is_port_reserved(_port) or not port_can_be_bound(_port):
254
+ return False
255
+ return True
256
+
257
+ for _ in range(max_attempts):
258
+ # try to find a suitable starting point (leave enough space at the end)
259
+ port_range_start = random.randint(
260
+ dynamic_port_range.start, dynamic_port_range.end - num_ports - 1
261
+ )
262
+ port_range = PortRange(port_range_start, port_range_start + num_ports - 1)
263
+
264
+ # check that each port in the range is available (has not been reserved and can be bound)
265
+ # we don't use dynamic_port_range.reserve_port because in case the port range check fails at some port
266
+ # all ports up until then would be reserved
267
+ if not _is_port_range_free(port_range):
268
+ continue
269
+
270
+ # port range found! mark them as reserved in the dynamic port range and return
271
+ for port in port_range:
272
+ dynamic_port_range.mark_reserved(port)
273
+ return port_range
274
+
275
+ raise PortNotAvailableException("reached max_attempts when trying to find port range")
276
+
277
+
278
+ def resolve_hostname(hostname: str) -> str | None:
279
+ """Resolve the given hostname and return its IP address, or None if it cannot be resolved."""
280
+ try:
281
+ return socket.gethostbyname(hostname)
282
+ except OSError:
283
+ return None
284
+
285
+
286
+ def is_ip_address(addr: str) -> bool:
287
+ try:
288
+ socket.inet_aton(addr)
289
+ return True
290
+ except OSError:
291
+ return False
292
+
293
+
294
+ def is_ipv4_address(address: str) -> bool:
295
+ """
296
+ Checks if passed string looks like an IPv4 address
297
+ :param address: Possible IPv4 address
298
+ :return: True if string looks like IPv4 address, False otherwise
299
+ """
300
+ return bool(re.match(IP_REGEX, address))
301
+
302
+
303
+ class PortNotAvailableException(Exception):
304
+ """Exception which indicates that the PortRange could not reserve a port."""
305
+
306
+ pass
307
+
308
+
309
+ class PortRange:
310
+ """Manages a range of ports that can be reserved and requested."""
311
+
312
+ def __init__(self, start: int, end: int):
313
+ """
314
+ Create a new port range. The port range is inclusive, meaning ``PortRange(5000,5005)`` is 6 ports
315
+ including both 5000 and 5005. This is different from ``range`` which is not inclusive, i.e.::
316
+
317
+ PortRange(5000, 5005).as_range() == range(5000, 5005 + 1)
318
+
319
+ :param start: the start port (inclusive)
320
+ :param end: the end of the range (inclusive).
321
+ """
322
+ self.start = start
323
+ self.end = end
324
+
325
+ # cache for locally available ports (ports are reserved for a short period of a few seconds)
326
+ self._ports_cache: MutableMapping[Port, Any] = CustomExpiryTTLCache(
327
+ maxsize=len(self),
328
+ ttl=DEFAULT_PORT_RESERVED_SECONDS,
329
+ )
330
+ self._ports_lock = threading.RLock()
331
+
332
+ def subrange(self, start: int = None, end: int = None) -> "PortRange":
333
+ """
334
+ Creates a new PortRange object from this range which is a sub-range of this port range. The new
335
+ object will use the same port cache and locks of this port range, so you can constrain port
336
+ reservations of an existing port range but have reservations synced between them.
337
+
338
+ :param start: the start of the subrange
339
+ :param end: the end of the subrange
340
+ :raises ValueError: if start or end are outside the current port range
341
+ :return: a new PortRange object synced to this one
342
+ """
343
+ start = start if start is not None else self.start
344
+ end = end if end is not None else self.end
345
+
346
+ if start < self.start:
347
+ raise ValueError(f"start not in range ({start} < {self.start})")
348
+ if end > self.end:
349
+ raise ValueError(f"end not in range ({end} < {self.end})")
350
+
351
+ # ensures that we return an instance of a subclass
352
+ port_range = type(self)(start, end)
353
+ port_range._ports_cache = self._ports_cache
354
+ port_range._ports_lock = self._ports_lock
355
+ return port_range
356
+
357
+ def as_range(self) -> range:
358
+ """
359
+ Returns a ``range(start, end+1)`` object representing this port range.
360
+
361
+ :return: a range
362
+ """
363
+ return range(self.start, self.end + 1)
364
+
365
+ def reserve_port(self, port: IntOrPort | None = None, duration: int | None = None) -> int:
366
+ """
367
+ Reserves the given port (if it is still free). If the given port is None, it reserves a free port from the
368
+ configured port range for external services. If a port is given, it has to be within the configured
369
+ range of external services (i.e., in the range [self.start, self.end)).
370
+
371
+ :param port: explicit port to check or None if a random port from the configured range should be selected
372
+ :param duration: the time in seconds the port is reserved for (defaults to a few seconds)
373
+ :return: reserved, free port number (int)
374
+ :raises PortNotAvailableException: if the given port is outside the configured range, it is already bound or
375
+ reserved, or if the given port is none and there is no free port in the configured service range.
376
+ """
377
+ ports_range = self.as_range()
378
+ port = Port.wrap(port) if port is not None else port
379
+ if port is not None and port.port not in ports_range:
380
+ raise PortNotAvailableException(
381
+ f"The requested port ({port}) is not in the port range ({ports_range})."
382
+ )
383
+ with self._ports_lock:
384
+ if port is not None:
385
+ return self._try_reserve_port(port, duration=duration)
386
+ else:
387
+ for port_in_range in ports_range:
388
+ try:
389
+ return self._try_reserve_port(port_in_range, duration=duration)
390
+ except PortNotAvailableException:
391
+ # We ignore the fact that this single port is reserved, we just check the next one
392
+ pass
393
+ raise PortNotAvailableException(
394
+ f"No free network ports available in {self!r} (currently reserved: %s)",
395
+ list(self._ports_cache.keys()),
396
+ )
397
+
398
+ def is_port_reserved(self, port: IntOrPort) -> bool:
399
+ """
400
+ Checks whether the port has been reserved in this PortRange. Does not check whether the port can be
401
+ bound or not, and does not check whether the port is in range.
402
+
403
+ :param port: the port to check
404
+ :return: true if the port is reserved within the range
405
+ """
406
+ port = Port.wrap(port)
407
+ return self._ports_cache.get(port) is not None
408
+
409
+ def mark_reserved(self, port: IntOrPort, duration: int = None):
410
+ """
411
+ Marks the given port as reserved for the given duration, regardless of whether it is free for not.
412
+
413
+ :param port: the port to reserve
414
+ :param duration: the duration
415
+ :raises ValueError: if the port is not in this port range
416
+ """
417
+ port = Port.wrap(port)
418
+
419
+ if port.port not in self.as_range():
420
+ raise ValueError(f"port {port} not in {self!r}")
421
+
422
+ with self._ports_lock:
423
+ # reserve the port for a short period of time
424
+ self._ports_cache[port] = "__reserved__"
425
+ if duration:
426
+ self._ports_cache.set_expiry(port, duration)
427
+
428
+ def _try_reserve_port(self, port: IntOrPort, duration: int) -> int:
429
+ """Checks if the given port is currently not reserved and can be bound."""
430
+ port = Port.wrap(port)
431
+
432
+ if self.is_port_reserved(port):
433
+ raise PortNotAvailableException(f"The given port ({port}) is already reserved.")
434
+ if not self._port_can_be_bound(port):
435
+ raise PortNotAvailableException(f"The given port ({port}) is already in use.")
436
+
437
+ self.mark_reserved(port, duration)
438
+ return port.port
439
+
440
+ def _port_can_be_bound(self, port: IntOrPort) -> bool:
441
+ """
442
+ Internal check whether the port can be bound. Will open a socket connection and see if the port is
443
+ available. Can be overwritten by subclasses to provide a custom implementation.
444
+
445
+ :param port: the port to check
446
+ :return: true if the port is free on the system
447
+ """
448
+ return port_can_be_bound(port)
449
+
450
+ def __len__(self):
451
+ return self.end - self.start + 1
452
+
453
+ def __iter__(self):
454
+ return self.as_range().__iter__()
455
+
456
+ def __repr__(self):
457
+ return f"PortRange({self.start}:{self.end})"
458
+
459
+
460
+ @singleton_factory
461
+ def get_docker_host_from_container() -> str:
462
+ """
463
+ Get the hostname/IP to connect to the host from within a Docker container (e.g., Lambda function).
464
+ The logic is roughly as follows:
465
+ 1. return `host.docker.internal` if we're running in host mode, in a non-Linux OS
466
+ 2. return the IP address that `host.docker.internal` (or alternatively `host.containers.internal`)
467
+ resolves to, if we're inside Docker
468
+ 3. return the Docker bridge IP (config.DOCKER_BRIDGE_IP) as a fallback, if option (2) fails
469
+ """
470
+ result = config.DOCKER_BRIDGE_IP
471
+ try:
472
+ if not config.is_in_docker and not config.is_in_linux:
473
+ # If we're running outside Docker (in host mode), and would like the Lambda containers to be able
474
+ # to access services running on the local machine, return `host.docker.internal` accordingly
475
+ result = "host.docker.internal"
476
+ if config.is_in_docker:
477
+ try:
478
+ result = socket.gethostbyname("host.docker.internal")
479
+ except OSError:
480
+ result = socket.gethostbyname("host.containers.internal")
481
+ except OSError:
482
+ # TODO if neither host resolves, we might be in linux. We could just use the default gateway then
483
+ pass
484
+ return result
485
+
486
+
487
+ def get_addressable_container_host(default_local_hostname: str = None) -> str:
488
+ """
489
+ Return the target host to address endpoints exposed by Docker containers, depending on
490
+ the current execution context.
491
+
492
+ If we're currently executing within Docker, then return get_docker_host_from_container(); otherwise, return
493
+ the value of `LOCALHOST_HOSTNAME`, assuming that container endpoints are exposed and accessible under localhost.
494
+
495
+ :param default_local_hostname: local hostname to return, if running outside Docker (defaults to LOCALHOST_HOSTNAME)
496
+ """
497
+ default_local_hostname = default_local_hostname or constants.LOCALHOST_HOSTNAME
498
+ return get_docker_host_from_container() if config.is_in_docker else default_local_hostname
499
+
500
+
501
+ def send_dns_query(
502
+ name: str,
503
+ port: int = 53,
504
+ ip_address: str = "127.0.0.1",
505
+ qtype: str = "A",
506
+ timeout: float = 1.0,
507
+ tcp: bool = False,
508
+ ) -> DNSRecord:
509
+ LOG.debug("querying %s:%d for name %s", ip_address, port, name)
510
+ request = DNSRecord.question(qname=name, qtype=qtype)
511
+ reply_bytes = request.send(dest=ip_address, port=port, tcp=tcp, timeout=timeout, ipv6=False)
512
+ return DNSRecord.parse(reply_bytes)
513
+
514
+
515
+ dynamic_port_range = PortRange(DYNAMIC_PORT_RANGE_START, DYNAMIC_PORT_RANGE_END)
516
+ """The dynamic port range."""
@@ -0,0 +1,19 @@
1
+ import argparse
2
+ import logging
3
+ from typing import NoReturn
4
+
5
+ LOG = logging.getLogger(__name__)
6
+
7
+
8
+ class NoExitArgumentParser(argparse.ArgumentParser):
9
+ """Implements the `exit_on_error=False` behavior introduced in Python 3.9 to support older Python versions
10
+ and prevents further SystemExit for other error categories.
11
+ * Limitations of error categories: https://stackoverflow.com/a/67891066/6875981
12
+ * ArgumentParser subclassing example: https://stackoverflow.com/a/59072378/6875981
13
+ """
14
+
15
+ def exit(self, status: int = ..., message: str | None = ...) -> NoReturn:
16
+ LOG.warning("Error in argument parser but preventing exit: %s", message)
17
+
18
+ def error(self, message: str) -> NoReturn:
19
+ raise NotImplementedError(f"Unsupported flag by this Docker client: {message}")
@@ -0,0 +1,49 @@
1
+ from typing import Any
2
+
3
+
4
+ def format_number(number: float, decimals: int = 2):
5
+ # Note: interestingly, f"{number:.3g}" seems to yield incorrect results in some cases.
6
+ # The logic below seems to be the most stable/reliable.
7
+ result = f"{number:.{decimals}f}"
8
+ if "." in result:
9
+ result = result.rstrip("0").rstrip(".")
10
+ return result
11
+
12
+
13
+ def is_number(s: Any) -> bool:
14
+ # booleans inherit from int
15
+ #
16
+ # >>> a.__class__.__mro__
17
+ # (<class 'bool'>, <class 'int'>, <class 'object'>)
18
+ if s is False or s is True:
19
+ return False
20
+
21
+ try:
22
+ float(s) # for int, long and float
23
+ return True
24
+ except (TypeError, ValueError):
25
+ return False
26
+
27
+
28
+ def to_number(s: Any) -> int | float:
29
+ """Cast the string representation of the given object to a number (int or float), or raise ValueError."""
30
+ try:
31
+ return int(str(s))
32
+ except ValueError:
33
+ return float(str(s))
34
+
35
+
36
+ def format_bytes(count: float, default: str = "n/a"):
37
+ """Format a bytes number as a human-readable unit, e.g., 1.3GB or 21.53MB"""
38
+ if not is_number(count):
39
+ return default
40
+ cnt = float(count)
41
+ if cnt < 0:
42
+ return default
43
+ units = ("B", "KB", "MB", "GB", "TB")
44
+ for unit in units:
45
+ if cnt < 1000 or unit == units[-1]:
46
+ # FIXME: will return '1e+03TB' for 1000TB
47
+ return f"{format_number(cnt, decimals=3)}{unit}"
48
+ cnt = cnt / 1000.0
49
+ return count