localstack-core 4.9.3.dev25__py3-none-any.whl → 4.9.3.dev27__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.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (70) hide show
  1. localstack/aws/handlers/metric_handler.py +41 -1
  2. localstack/cli/exceptions.py +1 -1
  3. localstack/cli/localstack.py +4 -4
  4. localstack/cli/lpm.py +3 -4
  5. localstack/cli/profiles.py +1 -2
  6. localstack/config.py +18 -12
  7. localstack/constants.py +4 -0
  8. localstack/packages/api.py +9 -8
  9. localstack/packages/core.py +2 -2
  10. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  11. localstack/testing/pytest/container.py +4 -5
  12. localstack/testing/pytest/fixtures.py +20 -19
  13. localstack/testing/pytest/marking.py +5 -4
  14. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  15. localstack/testing/pytest/util.py +1 -1
  16. localstack/testing/pytest/validation_tracking.py +1 -2
  17. localstack/utils/analytics/events.py +2 -2
  18. localstack/utils/analytics/metadata.py +1 -2
  19. localstack/utils/analytics/metrics/counter.py +6 -8
  20. localstack/utils/analytics/publisher.py +1 -2
  21. localstack/utils/analytics/service_request_aggregator.py +2 -2
  22. localstack/utils/archives.py +11 -11
  23. localstack/utils/aws/arns.py +7 -7
  24. localstack/utils/aws/aws_responses.py +7 -7
  25. localstack/utils/aws/aws_stack.py +2 -3
  26. localstack/utils/aws/message_forwarding.py +1 -2
  27. localstack/utils/aws/request_context.py +4 -5
  28. localstack/utils/batch_policy.py +3 -3
  29. localstack/utils/bootstrap.py +7 -7
  30. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  31. localstack/utils/collections.py +7 -8
  32. localstack/utils/config_listener.py +1 -1
  33. localstack/utils/container_networking.py +2 -3
  34. localstack/utils/container_utils/container_client.py +115 -131
  35. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  36. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  37. localstack/utils/diagnose.py +2 -3
  38. localstack/utils/docker_utils.py +3 -4
  39. localstack/utils/functions.py +3 -2
  40. localstack/utils/http.py +4 -5
  41. localstack/utils/json.py +3 -3
  42. localstack/utils/kinesis/kinesis_connector.py +2 -1
  43. localstack/utils/net.py +6 -6
  44. localstack/utils/no_exit_argument_parser.py +2 -2
  45. localstack/utils/numbers.py +2 -2
  46. localstack/utils/objects.py +6 -5
  47. localstack/utils/patch.py +2 -1
  48. localstack/utils/run.py +10 -9
  49. localstack/utils/scheduler.py +11 -11
  50. localstack/utils/server/tcp_proxy.py +2 -2
  51. localstack/utils/serving.py +2 -3
  52. localstack/utils/strings.py +10 -11
  53. localstack/utils/sync.py +2 -1
  54. localstack/utils/tagging.py +1 -4
  55. localstack/utils/testutil.py +5 -4
  56. localstack/utils/threads.py +2 -2
  57. localstack/utils/time.py +1 -2
  58. localstack/utils/urls.py +1 -3
  59. localstack/version.py +2 -2
  60. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/METADATA +2 -2
  61. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/RECORD +69 -69
  62. localstack_core-4.9.3.dev27.dist-info/plux.json +1 -0
  63. localstack_core-4.9.3.dev25.dist-info/plux.json +0 -1
  64. {localstack_core-4.9.3.dev25.data → localstack_core-4.9.3.dev27.data}/scripts/localstack +0 -0
  65. {localstack_core-4.9.3.dev25.data → localstack_core-4.9.3.dev27.data}/scripts/localstack-supervisor +0 -0
  66. {localstack_core-4.9.3.dev25.data → localstack_core-4.9.3.dev27.data}/scripts/localstack.bat +0 -0
  67. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/WHEEL +0 -0
  68. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/entry_points.txt +0 -0
  69. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/licenses/LICENSE.txt +0 -0
  70. {localstack_core-4.9.3.dev25.dist-info → localstack_core-4.9.3.dev27.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Union
1
+ from typing import Any
2
2
 
3
3
 
4
4
  def format_number(number: float, decimals: int = 2):
@@ -25,7 +25,7 @@ def is_number(s: Any) -> bool:
25
25
  return False
26
26
 
27
27
 
28
- def to_number(s: Any) -> Union[int, float]:
28
+ def to_number(s: Any) -> int | float:
29
29
  """Cast the string representation of the given object to a number (int or float), or raise ValueError."""
30
30
  try:
31
31
  return int(str(s))
@@ -1,12 +1,13 @@
1
1
  import functools
2
2
  import re
3
3
  import threading
4
- from typing import Any, Callable, Generic, Optional, TypeVar, Union
4
+ from collections.abc import Callable
5
+ from typing import Any, Generic, TypeVar
5
6
 
6
7
  from .collections import ensure_list
7
8
  from .strings import first_char_to_lower, first_char_to_upper
8
9
 
9
- ComplexType = Union[list, dict, object]
10
+ ComplexType = list | dict | object
10
11
 
11
12
  _T = TypeVar("_T")
12
13
 
@@ -16,7 +17,7 @@ class Value(Generic[_T]):
16
17
  Simple value container.
17
18
  """
18
19
 
19
- value: Optional[_T]
20
+ value: _T | None
20
21
 
21
22
  def __init__(self, value: _T = None) -> None:
22
23
  self.value = value
@@ -30,7 +31,7 @@ class Value(Generic[_T]):
30
31
  def is_set(self) -> bool:
31
32
  return self.value is not None
32
33
 
33
- def get(self) -> Optional[_T]:
34
+ def get(self) -> _T | None:
34
35
  return self.value
35
36
 
36
37
  def __bool__(self):
@@ -136,7 +137,7 @@ def fully_qualified_class_name(klass: type) -> str:
136
137
  return f"{klass.__module__}.{klass.__name__}"
137
138
 
138
139
 
139
- def not_none_or(value: Optional[Any], alternative: Any) -> Any:
140
+ def not_none_or(value: Any | None, alternative: Any) -> Any:
140
141
  """Return 'value' if it is not None, or 'alternative' otherwise."""
141
142
  return value if value is not None else alternative
142
143
 
localstack/utils/patch.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import functools
2
2
  import inspect
3
3
  import types
4
- from typing import Any, Callable
4
+ from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
7
 
7
8
  def get_defining_object(method):
localstack/utils/run.py CHANGED
@@ -7,9 +7,10 @@ import subprocess
7
7
  import sys
8
8
  import threading
9
9
  import time
10
+ from collections.abc import Callable
10
11
  from functools import lru_cache
11
12
  from queue import Queue
12
- from typing import Any, AnyStr, Callable, Optional, Union
13
+ from typing import Any, AnyStr
13
14
 
14
15
  from localstack import config
15
16
 
@@ -23,19 +24,19 @@ LOG = logging.getLogger(__name__)
23
24
 
24
25
 
25
26
  def run(
26
- cmd: Union[str, list[str]],
27
+ cmd: str | list[str],
27
28
  print_error=True,
28
29
  asynchronous=False,
29
30
  stdin=False,
30
31
  stderr=subprocess.STDOUT,
31
32
  outfile=None,
32
- env_vars: Optional[dict[AnyStr, AnyStr]] = None,
33
+ env_vars: dict[AnyStr, AnyStr] | None = None,
33
34
  inherit_cwd=False,
34
35
  inherit_env=True,
35
36
  tty=False,
36
37
  shell=True,
37
38
  cwd: str = None,
38
- ) -> Union[str, subprocess.Popen]:
39
+ ) -> str | subprocess.Popen:
39
40
  LOG.debug("Executing command: %s", cmd)
40
41
  env_dict = os.environ.copy() if inherit_env else {}
41
42
  if env_vars:
@@ -202,7 +203,7 @@ def get_os_user() -> str:
202
203
  return run("whoami").strip()
203
204
 
204
205
 
205
- def to_str(obj: Union[str, bytes], errors="strict"):
206
+ def to_str(obj: str | bytes, errors="strict"):
206
207
  return obj.decode(config.DEFAULT_ENCODING, errors) if isinstance(obj, bytes) else obj
207
208
 
208
209
 
@@ -211,9 +212,9 @@ class ShellCommandThread(FuncThread):
211
212
 
212
213
  def __init__(
213
214
  self,
214
- cmd: Union[str, list[str]],
215
+ cmd: str | list[str],
215
216
  params: Any = None,
216
- outfile: Union[str, int] = None,
217
+ outfile: str | int = None,
217
218
  env_vars: dict[str, str] = None,
218
219
  stdin: bool = False,
219
220
  auto_restart: bool = False,
@@ -223,8 +224,8 @@ class ShellCommandThread(FuncThread):
223
224
  log_listener: Callable = None,
224
225
  stop_listener: Callable = None,
225
226
  strip_color: bool = False,
226
- name: Optional[str] = None,
227
- cwd: Optional[str] = None,
227
+ name: str | None = None,
228
+ cwd: str | None = None,
228
229
  ):
229
230
  params = params if params is not None else {}
230
231
  env_vars = env_vars if env_vars is not None else {}
@@ -1,9 +1,9 @@
1
1
  import queue
2
2
  import threading
3
3
  import time
4
- from collections.abc import Mapping
4
+ from collections.abc import Callable, Mapping
5
5
  from concurrent.futures import Executor
6
- from typing import Any, Callable, Optional, Union
6
+ from typing import Any
7
7
 
8
8
 
9
9
  class ScheduledTask:
@@ -14,12 +14,12 @@ class ScheduledTask:
14
14
  def __init__(
15
15
  self,
16
16
  task: Callable,
17
- period: Optional[float] = None,
17
+ period: float | None = None,
18
18
  fixed_rate: bool = True,
19
- start: Optional[float] = None,
19
+ start: float | None = None,
20
20
  on_error: Callable[[Exception], None] = None,
21
- args: Optional[Union[tuple, list]] = None,
22
- kwargs: Optional[Mapping[str, Any]] = None,
21
+ args: tuple | list | None = None,
22
+ kwargs: Mapping[str, Any] | None = None,
23
23
  ) -> None:
24
24
  super().__init__()
25
25
  self.task = task
@@ -76,7 +76,7 @@ class Scheduler:
76
76
 
77
77
  POISON = (-1, "__POISON__")
78
78
 
79
- def __init__(self, executor: Optional[Executor] = None) -> None:
79
+ def __init__(self, executor: Executor | None = None) -> None:
80
80
  """
81
81
  Creates a new Scheduler. If an executor is passed, then that executor will be used to run the scheduled tasks
82
82
  asynchronously, otherwise they will be executed synchronously inside the event loop. Running tasks
@@ -94,12 +94,12 @@ class Scheduler:
94
94
  def schedule(
95
95
  self,
96
96
  func: Callable,
97
- period: Optional[float] = None,
97
+ period: float | None = None,
98
98
  fixed_rate: bool = True,
99
- start: Optional[float] = None,
99
+ start: float | None = None,
100
100
  on_error: Callable[[Exception], None] = None,
101
- args: Optional[Union[tuple, list[Any]]] = None,
102
- kwargs: Optional[Mapping[str, Any]] = None,
101
+ args: tuple | list[Any] | None = None,
102
+ kwargs: Mapping[str, Any] | None = None,
103
103
  ) -> ScheduledTask:
104
104
  """
105
105
  Schedules a given task (function call).
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  import select
3
3
  import socket
4
+ from collections.abc import Callable
4
5
  from concurrent.futures import ThreadPoolExecutor
5
- from typing import Callable
6
6
 
7
7
  from localstack.utils.serving import Server
8
8
 
@@ -93,7 +93,7 @@ class TCPProxy(Server):
93
93
  try:
94
94
  src_socket, _ = self._server_socket.accept()
95
95
  self._thread_pool.submit(self._handle_request, src_socket)
96
- except socket.timeout:
96
+ except TimeoutError:
97
97
  pass
98
98
  except OSError as e:
99
99
  # avoid creating an error message if OSError is thrown due to socket closing
@@ -1,7 +1,6 @@
1
1
  import abc
2
2
  import logging
3
3
  import threading
4
- from typing import Optional
5
4
 
6
5
  from localstack.utils.net import is_port_open
7
6
  from localstack.utils.sync import poll_condition
@@ -21,7 +20,7 @@ class Server(abc.ABC):
21
20
 
22
21
  def __init__(self, port: int, host: str = "localhost") -> None:
23
22
  super().__init__()
24
- self._thread: Optional[FuncThread] = None
23
+ self._thread: FuncThread | None = None
25
24
 
26
25
  self._lifecycle_lock = threading.RLock()
27
26
  self._stopped = threading.Event()
@@ -46,7 +45,7 @@ class Server(abc.ABC):
46
45
  def url(self):
47
46
  return f"{self.protocol}://{self.host}:{self.port}"
48
47
 
49
- def get_error(self) -> Optional[Exception]:
48
+ def get_error(self) -> Exception | None:
50
49
  """
51
50
  If the thread running the server returned with an Exception, then this function will return that exception.
52
51
  """
@@ -7,7 +7,6 @@ import re
7
7
  import string
8
8
  import uuid
9
9
  import zlib
10
- from typing import Union
11
10
 
12
11
  from localstack.config import DEFAULT_ENCODING
13
12
 
@@ -28,13 +27,13 @@ REGEX_UNPRINTABLE_CHARS = re.compile(
28
27
  )
29
28
 
30
29
 
31
- def to_str(obj: Union[str, bytes], encoding: str = DEFAULT_ENCODING, errors="strict") -> str:
30
+ def to_str(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> str:
32
31
  """If ``obj`` is an instance of ``binary_type``, return
33
32
  ``obj.decode(encoding, errors)``, otherwise return ``obj``"""
34
33
  return obj.decode(encoding, errors) if isinstance(obj, bytes) else obj
35
34
 
36
35
 
37
- def to_bytes(obj: Union[str, bytes], encoding: str = DEFAULT_ENCODING, errors="strict") -> bytes:
36
+ def to_bytes(obj: str | bytes, encoding: str = DEFAULT_ENCODING, errors="strict") -> bytes:
38
37
  """If ``obj`` is an instance of ``text_type``, return
39
38
  ``obj.encode(encoding, errors)``, otherwise return ``obj``"""
40
39
  return obj.encode(encoding, errors) if isinstance(obj, str) else obj
@@ -86,7 +85,7 @@ def canonicalize_bool_to_str(val: bool) -> str:
86
85
  return "true" if str(val).lower() == "true" else "false"
87
86
 
88
87
 
89
- def convert_to_printable_chars(value: Union[list, dict, str]) -> str:
88
+ def convert_to_printable_chars(value: list | dict | str) -> str:
90
89
  """Removes all unprintable characters from the given string."""
91
90
  from localstack.utils.objects import recurse_object
92
91
 
@@ -148,19 +147,19 @@ def long_uid() -> str:
148
147
  return str(uuid.uuid4())
149
148
 
150
149
 
151
- def md5(string: Union[str, bytes]) -> str:
150
+ def md5(string: str | bytes) -> str:
152
151
  m = hashlib.md5()
153
152
  m.update(to_bytes(string))
154
153
  return m.hexdigest()
155
154
 
156
155
 
157
- def checksum_crc32(string: Union[str, bytes]) -> str:
156
+ def checksum_crc32(string: str | bytes) -> str:
158
157
  bytes = to_bytes(string)
159
158
  checksum = zlib.crc32(bytes)
160
159
  return base64.b64encode(checksum.to_bytes(4, "big")).decode()
161
160
 
162
161
 
163
- def checksum_crc32c(string: Union[str, bytes]):
162
+ def checksum_crc32c(string: str | bytes):
164
163
  # import botocore locally here to avoid a dependency of the CLI to botocore
165
164
  from botocore.httpchecksum import CrtCrc32cChecksum
166
165
 
@@ -169,7 +168,7 @@ def checksum_crc32c(string: Union[str, bytes]):
169
168
  return base64.b64encode(checksum.digest()).decode()
170
169
 
171
170
 
172
- def checksum_crc64nvme(string: Union[str, bytes]):
171
+ def checksum_crc64nvme(string: str | bytes):
173
172
  # import botocore locally here to avoid a dependency of the CLI to botocore
174
173
  from botocore.httpchecksum import CrtCrc64NvmeChecksum
175
174
 
@@ -178,12 +177,12 @@ def checksum_crc64nvme(string: Union[str, bytes]):
178
177
  return base64.b64encode(checksum.digest()).decode()
179
178
 
180
179
 
181
- def hash_sha1(string: Union[str, bytes]) -> str:
180
+ def hash_sha1(string: str | bytes) -> str:
182
181
  digest = hashlib.sha1(to_bytes(string)).digest()
183
182
  return base64.b64encode(digest).decode()
184
183
 
185
184
 
186
- def hash_sha256(string: Union[str, bytes]) -> str:
185
+ def hash_sha256(string: str | bytes) -> str:
187
186
  digest = hashlib.sha256(to_bytes(string)).digest()
188
187
  return base64.b64encode(digest).decode()
189
188
 
@@ -192,7 +191,7 @@ def base64_to_hex(b64_string: str) -> bytes:
192
191
  return binascii.hexlify(base64.b64decode(b64_string))
193
192
 
194
193
 
195
- def base64_decode(data: Union[str, bytes]) -> bytes:
194
+ def base64_decode(data: str | bytes) -> bytes:
196
195
  """Decode base64 data - with optional padding, and able to handle urlsafe encoding (containing -/_)."""
197
196
  data = to_str(data)
198
197
  missing_padding = len(data) % 4
localstack/utils/sync.py CHANGED
@@ -4,7 +4,8 @@ import functools
4
4
  import threading
5
5
  import time
6
6
  from collections import defaultdict
7
- from typing import Callable, Literal, TypeVar
7
+ from collections.abc import Callable
8
+ from typing import Literal, TypeVar
8
9
 
9
10
 
10
11
  class ShortCircuitWaitException(Exception):
@@ -1,6 +1,3 @@
1
- from typing import Optional
2
-
3
-
4
1
  class TaggingService:
5
2
  key_field: str
6
3
  value_field: str
@@ -17,7 +14,7 @@ class TaggingService:
17
14
 
18
15
  self.tags = {}
19
16
 
20
- def list_tags_for_resource(self, arn: str, root_name: Optional[str] = None):
17
+ def list_tags_for_resource(self, arn: str, root_name: str | None = None):
21
18
  root_name = root_name or "Tags"
22
19
 
23
20
  result = []
@@ -7,7 +7,8 @@ import re
7
7
  import shutil
8
8
  import tempfile
9
9
  import time
10
- from typing import Any, Callable, Optional
10
+ from collections.abc import Callable
11
+ from typing import Any
11
12
 
12
13
  from localstack.aws.api.lambda_ import Runtime
13
14
  from localstack.aws.connect import connect_externally_to, connect_to
@@ -20,7 +21,7 @@ from localstack.utils.urls import localstack_host
20
21
  try:
21
22
  from typing import Literal
22
23
  except ImportError:
23
- from typing_extensions import Literal
24
+ from typing import Literal
24
25
 
25
26
  import boto3
26
27
  import requests
@@ -548,7 +549,7 @@ def list_all_log_events(log_group_name: str, logs_client=None) -> list[dict]:
548
549
  def get_lambda_log_events(
549
550
  function_name,
550
551
  delay_time=DEFAULT_GET_LOG_EVENTS_DELAY,
551
- regex_filter: Optional[str] = None,
552
+ regex_filter: str | None = None,
552
553
  log_group=None,
553
554
  logs_client=None,
554
555
  ):
@@ -596,7 +597,7 @@ def list_all_resources(
596
597
  page_function: Callable[[dict], Any],
597
598
  last_token_attr_name: str,
598
599
  list_attr_name: str,
599
- next_token_attr_name: Optional[str] = None,
600
+ next_token_attr_name: str | None = None,
600
601
  ) -> list:
601
602
  """
602
603
  List all available resources by loading all available pages using `page_function`.
@@ -3,9 +3,9 @@ import inspect
3
3
  import logging
4
4
  import threading
5
5
  import traceback
6
+ from collections.abc import Callable
6
7
  from concurrent.futures import Future
7
8
  from multiprocessing.dummy import Pool
8
- from typing import Callable, Optional
9
9
 
10
10
  LOG = logging.getLogger(__name__)
11
11
 
@@ -26,7 +26,7 @@ class FuncThread(threading.Thread):
26
26
  params=None,
27
27
  quiet=False,
28
28
  on_stop: Callable[["FuncThread"], None] = None,
29
- name: Optional[str] = None,
29
+ name: str | None = None,
30
30
  daemon=True,
31
31
  ):
32
32
  global counter
localstack/utils/time.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import time
2
2
  from datetime import date, datetime, timezone, tzinfo
3
- from typing import Optional
4
3
  from zoneinfo import ZoneInfo
5
4
 
6
5
  TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"
@@ -64,7 +63,7 @@ def parse_timestamp(ts_str: str) -> datetime:
64
63
  raise Exception(f"Unable to parse timestamp string with any known formats: {ts_str}")
65
64
 
66
65
 
67
- def now(millis: bool = False, tz: Optional[tzinfo] = None) -> int:
66
+ def now(millis: bool = False, tz: tzinfo | None = None) -> int:
68
67
  return mktime(datetime.now(tz=tz), millis=millis)
69
68
 
70
69
 
localstack/utils/urls.py CHANGED
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from localstack import config
4
2
  from localstack.config import HostAndPort
5
3
 
@@ -12,7 +10,7 @@ def hostname_from_url(url: str) -> str:
12
10
  return url.split("://")[-1].split("/")[0].split(":")[0]
13
11
 
14
12
 
15
- def localstack_host(custom_port: Optional[int] = None) -> HostAndPort:
13
+ def localstack_host(custom_port: int | None = None) -> HostAndPort:
16
14
  """
17
15
  Determine the host and port to return to the user based on:
18
16
  - the user's configuration (e.g environment variable overrides)
localstack/version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '4.9.3.dev25'
32
- __version_tuple__ = version_tuple = (4, 9, 3, 'dev25')
31
+ __version__ = version = '4.9.3.dev27'
32
+ __version_tuple__ = version_tuple = (4, 9, 3, 'dev27')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localstack-core
3
- Version: 4.9.3.dev25
3
+ Version: 4.9.3.dev27
4
4
  Summary: The core library and runtime of LocalStack
5
5
  Author-email: LocalStack Contributors <info@localstack.cloud>
6
6
  License-Expression: Apache-2.0
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Topic :: Internet
14
14
  Classifier: Topic :: Software Development :: Testing
15
15
  Classifier: Topic :: System :: Emulators
16
- Requires-Python: >=3.9
16
+ Requires-Python: >=3.10
17
17
  License-File: LICENSE.txt
18
18
  Requires-Dist: build
19
19
  Requires-Dist: click>=7.1