nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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 (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/package.py CHANGED
@@ -6,33 +6,37 @@ import platform
6
6
  import re
7
7
  import shutil
8
8
  import subprocess
9
+ import sys
9
10
  import tarfile
10
11
  import tempfile
11
- from typing import Optional
12
12
 
13
- from nextmv.cloud.manifest import FILE_NAME, Manifest, ManifestBuild, ManifestType
13
+ import rich
14
+
14
15
  from nextmv.logger import log
16
+ from nextmv.manifest import MANIFEST_FILE_NAME, Manifest, ManifestBuild, ManifestType
15
17
  from nextmv.model import Model, ModelConfiguration, _cleanup_python_model
16
18
 
17
19
  _MANDATORY_FILES_PER_TYPE = {
18
20
  ManifestType.PYTHON: ["main.py"],
19
21
  ManifestType.GO: ["main"],
22
+ ManifestType.BINARY: ["main"],
20
23
  ManifestType.JAVA: ["main.jar"],
21
24
  }
22
25
 
23
26
 
24
- def _package(
27
+ def _package( # noqa: C901 # complexity attributed to printing.
25
28
  app_dir: str,
26
29
  manifest: Manifest,
27
- model: Optional[Model] = None,
28
- model_configuration: Optional[ModelConfiguration] = None,
30
+ model: Model | None = None,
31
+ model_configuration: ModelConfiguration | None = None,
29
32
  verbose: bool = False,
33
+ rich_print: bool = False,
30
34
  ) -> tuple[str, str]:
31
- """Package the app into a tarball.."""
35
+ """Package the app into a tarball."""
32
36
 
33
37
  with tempfile.TemporaryDirectory(prefix="nextmv-temp-") as temp_dir:
34
38
  if manifest.type == ManifestType.PYTHON:
35
- __handle_python(app_dir, temp_dir, manifest, model, model_configuration, verbose)
39
+ __handle_python(app_dir, temp_dir, manifest, model, model_configuration, verbose, rich_print)
36
40
 
37
41
  found, missing, files = __find_files(app_dir, manifest.files)
38
42
  __confirm_mandatory_files(manifest, found)
@@ -55,7 +59,13 @@ def _package(
55
59
  raise Exception(f"error copying asset files {file['absolute_path']}: {e}") from e
56
60
 
57
61
  if verbose:
58
- log(f'📋 Copied files listed in "{FILE_NAME}" manifest.')
62
+ if rich_print:
63
+ rich.print(
64
+ f":clipboard: Copied files listed in [magenta]{MANIFEST_FILE_NAME}[/magenta] manifest.",
65
+ file=sys.stderr,
66
+ )
67
+ else:
68
+ log(f'📋 Copied files listed in "{MANIFEST_FILE_NAME}" manifest.')
59
69
 
60
70
  if manifest.type == ManifestType.PYTHON:
61
71
  _cleanup_python_model(app_dir, model_configuration, verbose)
@@ -66,17 +76,31 @@ def _package(
66
76
  if verbose:
67
77
  try:
68
78
  size = __human_friendly_file_size(tar_file)
69
- log(f"📦 Packaged application ({file_count_msg}, {size}).")
79
+ if rich_print:
80
+ rich.print(
81
+ ":package: Packaged application "
82
+ f"([magenta]{file_count_msg}[/magenta], [magenta]{size}[/magenta]).",
83
+ file=sys.stderr,
84
+ )
85
+ else:
86
+ log(f"📦 Packaged application ({file_count_msg}, {size}).")
70
87
  except Exception:
71
- log(f"📦 Packaged application ({file_count_msg}).")
88
+ if rich_print:
89
+ rich.print(
90
+ f":package: Packaged application ([magenta]{file_count_msg}[/magenta]).",
91
+ file=sys.stderr,
92
+ )
93
+ else:
94
+ log(f"📦 Packaged application ({file_count_msg}).")
72
95
 
73
96
  return tar_file, output_dir
74
97
 
75
98
 
76
99
  def _run_build_command(
77
100
  app_dir: str,
78
- manifest_build: Optional[ManifestBuild] = None,
101
+ manifest_build: ManifestBuild | None = None,
79
102
  verbose: bool = False,
103
+ rich_print: bool = False,
80
104
  ) -> None:
81
105
  """Run the build command specified in the manifest."""
82
106
 
@@ -85,7 +109,12 @@ def _run_build_command(
85
109
 
86
110
  elements = manifest_build.command.split(" ")
87
111
  command_str = " ".join(elements)
88
- log(f'🚧 Running build command: "{command_str}"')
112
+
113
+ if verbose:
114
+ if rich_print:
115
+ rich.print(f":construction: Running build command: [magenta]{command_str}[/magenta]", file=sys.stderr)
116
+ else:
117
+ log(f'🚧 Running build command: "{command_str}"')
89
118
  try:
90
119
  result = subprocess.run(
91
120
  elements,
@@ -103,22 +132,38 @@ def _run_build_command(
103
132
  log(result.stdout)
104
133
 
105
134
 
135
+ def _get_shell_command_elements(pre_push_command):
136
+ """Get the shell command elements based on the operating system."""
137
+ # Check if we're in a Unix-like shell (including MINGW on Windows)
138
+ if "SHELL" in os.environ and shutil.which("bash"):
139
+ return ["bash", "-c", pre_push_command]
140
+ # Default to cmd on Windows
141
+ elif platform.system() == "Windows":
142
+ return ["cmd", "/c", pre_push_command]
143
+ # Default to sh on Unix-like systems (Linux, macOS)
144
+ else:
145
+ return ["sh", "-c", pre_push_command]
146
+
147
+
106
148
  def _run_pre_push_command(
107
149
  app_dir: str,
108
- pre_push_command: Optional[str] = None,
150
+ pre_push_command: str | None = None,
109
151
  verbose: bool = False,
152
+ rich_print: bool = False,
110
153
  ) -> None:
111
154
  """Run the pre-push command specified in the manifest."""
112
155
 
113
156
  if pre_push_command is None or pre_push_command == "":
114
157
  return
115
158
 
116
- elements = ["bash", "-c", pre_push_command]
117
- if platform.system() == "Windows":
118
- elements = ["cmd", "/c", pre_push_command]
159
+ elements = _get_shell_command_elements(pre_push_command)
119
160
 
120
161
  command_str = " ".join(elements)
121
- log(f'🔨 Running pre-push command: "{command_str}"')
162
+ if verbose:
163
+ if rich_print:
164
+ rich.print(f":hammer: Running pre-push command: [magenta]{command_str}[/magenta]", file=sys.stderr)
165
+ else:
166
+ log(f'🔨 Running pre-push command: "{command_str}"')
122
167
  try:
123
168
  result = subprocess.run(
124
169
  elements,
@@ -194,8 +239,15 @@ def __find_files(
194
239
  def __confirm_mandatory_files(manifest: Manifest, present_files: list[str]) -> None:
195
240
  """Confirm that all mandatory files are present in the given list of files."""
196
241
 
197
- mandatory_files = _MANDATORY_FILES_PER_TYPE[manifest.type]
198
242
  found_files = {os.path.normpath(file): True for file in present_files}
243
+
244
+ # Check for mandatory files (if a custom execution config is provided we check the
245
+ # custom entrypoint instead)
246
+ mandatory_files = []
247
+ if manifest.execution is None or manifest.execution.entrypoint is None:
248
+ mandatory_files = _MANDATORY_FILES_PER_TYPE[manifest.type]
249
+ else:
250
+ mandatory_files.append(os.path.normpath(manifest.execution.entrypoint))
199
251
  missing_files = [file for file in mandatory_files if file not in found_files]
200
252
 
201
253
  if missing_files:
@@ -206,23 +258,30 @@ def __handle_python(
206
258
  app_dir: str,
207
259
  temp_dir: str,
208
260
  manifest: Manifest,
209
- model: Optional[Model] = None,
210
- model_configuration: Optional[ModelConfiguration] = None,
261
+ model: Model | None = None,
262
+ model_configuration: ModelConfiguration | None = None,
211
263
  verbose: bool = False,
264
+ rich_print: bool = False,
212
265
  ) -> None:
213
266
  """Handles the Python-specific packaging logic."""
214
267
 
215
268
  if model is not None and model_configuration is not None:
216
269
  if verbose:
217
- log("🔮 Encoding Python model.")
270
+ if rich_print:
271
+ rich.print(":crystal_ball: Encoding Python model.", file=sys.stderr)
272
+ else:
273
+ log("🔮 Encoding Python model.")
218
274
  model.save(app_dir, model_configuration)
219
275
 
220
276
  if verbose:
221
- log("🐍 Bundling Python dependencies.")
277
+ if rich_print:
278
+ rich.print(":snake: Bundling Python dependencies.", file=sys.stderr)
279
+ else:
280
+ log("🐍 Bundling Python dependencies.")
222
281
  __install_dependencies(manifest, app_dir, temp_dir)
223
282
 
224
283
 
225
- def __install_dependencies(
284
+ def __install_dependencies( # noqa: C901 # complexity
226
285
  manifest: Manifest,
227
286
  app_dir: str,
228
287
  temp_dir: str,
@@ -233,46 +292,91 @@ def __install_dependencies(
233
292
  return
234
293
 
235
294
  pip_requirements = manifest.python.pip_requirements
295
+
236
296
  if pip_requirements is None or pip_requirements == "":
297
+ # If no pip requirements are specified, we do not install any dependencies.
237
298
  return
238
299
 
239
- if not os.path.isfile(os.path.join(app_dir, pip_requirements)):
240
- raise FileNotFoundError(f"pip requirements file '{pip_requirements}' not found in '{app_dir}'")
300
+ if isinstance(pip_requirements, list):
301
+ # If pip_requirements is a list, we write it to a temporary file so that we can
302
+ # pass it to pip.
303
+ pip_requirements_file = os.path.join(temp_dir, "requirements.txt")
304
+ with open(pip_requirements_file, "w") as f:
305
+ for requirement in pip_requirements:
306
+ f.write(requirement + "\n")
307
+ pip_requirements = pip_requirements_file
308
+ elif isinstance(pip_requirements, str):
309
+ # If pip_requirements is a string, we expect it to be a file path to a
310
+ # requirements file.
311
+ pip_requirements = pip_requirements.strip()
312
+ if not os.path.isfile(os.path.join(app_dir, pip_requirements)):
313
+ raise FileNotFoundError(f"pip requirements file '{pip_requirements}' not found in '{app_dir}'")
314
+
315
+ platform_filter = []
316
+ if not manifest.python.arch or manifest.python.arch == "arm64":
317
+ platform_filter.extend(
318
+ [
319
+ "--platform=manylinux2014_aarch64",
320
+ "--platform=manylinux_2_17_aarch64",
321
+ "--platform=manylinux_2_24_aarch64",
322
+ "--platform=manylinux_2_26_aarch64",
323
+ "--platform=manylinux_2_28_aarch64",
324
+ "--platform=manylinux_2_34_aarch64",
325
+ "--platform=linux_aarch64",
326
+ ]
327
+ )
328
+ elif manifest.python.arch == "amd64":
329
+ platform_filter.extend(
330
+ [
331
+ "--platform=manylinux2014_x86_64",
332
+ "--platform=manylinux_2_17_x86_64",
333
+ "--platform=manylinux_2_24_x86_64",
334
+ "--platform=manylinux_2_26_x86_64",
335
+ "--platform=manylinux_2_28_x86_64",
336
+ "--platform=manylinux_2_34_x86_64",
337
+ "--platform=linux_x86_64",
338
+ ]
339
+ )
340
+ else:
341
+ raise Exception(f"unknown architecture '{manifest.python.arch}' specified in manifest")
342
+
343
+ version_filter = ["--python-version=3.11"]
344
+ if manifest.python.version:
345
+ __confirm_python_bundling_version(manifest.python.version)
346
+ version_filter = [f"--python-version={manifest.python.version}"]
241
347
 
242
348
  py_cmd = __get_python_command()
243
349
  dep_dir = os.path.join(".nextmv", "python", "deps")
244
- command = [
245
- py_cmd,
246
- "-m",
247
- "pip",
248
- "install",
249
- "-r",
250
- pip_requirements,
251
- "--platform=manylinux2014_aarch64",
252
- "--platform=manylinux_2_17_aarch64",
253
- "--platform=manylinux_2_24_aarch64",
254
- "--platform=manylinux_2_28_aarch64",
255
- "--platform=linux_aarch64",
256
- "--only-binary=:all:",
257
- "--python-version=3.11",
258
- "--implementation=cp",
259
- "--upgrade",
260
- "--no-warn-conflicts",
261
- "--target",
262
- os.path.join(temp_dir, dep_dir),
263
- "--no-user", # We explicitly avoid user mode (mainly to fix issues with Windows store Python installations)
264
- "--no-input",
265
- "--quiet",
266
- ]
350
+ command = (
351
+ [
352
+ py_cmd,
353
+ "-m",
354
+ "pip",
355
+ "install",
356
+ "-r",
357
+ pip_requirements,
358
+ "--only-binary=:all:",
359
+ "--implementation=cp",
360
+ "--upgrade",
361
+ "--no-warn-conflicts",
362
+ "--target",
363
+ os.path.join(temp_dir, dep_dir),
364
+ "--no-user", # We explicitly avoid user mode (mainly to fix issues with Windows store Python installations)
365
+ "--no-input",
366
+ "--quiet",
367
+ ]
368
+ + platform_filter
369
+ + version_filter
370
+ )
267
371
  result = subprocess.run(
268
372
  command,
269
373
  cwd=app_dir,
374
+ stdout=subprocess.PIPE,
375
+ stderr=subprocess.STDOUT, # Merge stderr into stdout
270
376
  text=True,
271
- capture_output=True,
272
- check=True,
273
377
  )
274
378
  if result.returncode != 0:
275
- raise Exception(f"error installing dependencies: {result.stderr}")
379
+ raise Exception(f"error installing dependencies: {os.linesep}{result.stdout}")
276
380
 
277
381
 
278
382
  def __run_command(binary: str, dir: str, redirect_out_err: bool, *arguments: str) -> str:
@@ -361,10 +465,21 @@ def __confirm_python_version(output: str) -> None:
361
465
  except ValueError:
362
466
  major, minor = map(int, version.split("."))
363
467
 
364
- if major == 3 and minor >= 9:
468
+ if major == 3 and minor >= 10:
365
469
  return
366
470
 
367
- raise Exception("python version 3.9 or higher is required")
471
+ raise Exception("python version 3.10 or higher is required")
472
+
473
+
474
+ def __confirm_python_bundling_version(version: str) -> None:
475
+ # Only accept versions in the form "major.minor" where both are integers
476
+ re_version = re.compile(r"^(\d+)\.(\d+)$")
477
+ match = re_version.fullmatch(version)
478
+ if match:
479
+ major, minor = int(match.group(1)), int(match.group(2))
480
+ if major == 3 and minor >= 10:
481
+ return
482
+ raise Exception(f"python version 3.10 or higher is required for bundling, got {version}")
368
483
 
369
484
 
370
485
  def __compress_tar(source: str, target: str) -> tuple[str, int]: