fractal-server 2.11.1__py3-none-any.whl → 2.12.0a0__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 (61) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/__init__.py +0 -2
  3. fractal_server/app/models/linkuserproject.py +0 -9
  4. fractal_server/app/routes/aux/_job.py +1 -3
  5. fractal_server/app/runner/filenames.py +0 -2
  6. fractal_server/app/runner/shutdown.py +3 -27
  7. fractal_server/config.py +1 -15
  8. fractal_server/main.py +1 -12
  9. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +67 -0
  10. fractal_server/string_tools.py +0 -21
  11. fractal_server/tasks/utils.py +0 -24
  12. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0a0.dist-info}/METADATA +1 -1
  13. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0a0.dist-info}/RECORD +16 -60
  14. fractal_server/app/models/v1/__init__.py +0 -13
  15. fractal_server/app/models/v1/dataset.py +0 -71
  16. fractal_server/app/models/v1/job.py +0 -101
  17. fractal_server/app/models/v1/project.py +0 -29
  18. fractal_server/app/models/v1/state.py +0 -34
  19. fractal_server/app/models/v1/task.py +0 -85
  20. fractal_server/app/models/v1/workflow.py +0 -133
  21. fractal_server/app/routes/admin/v1.py +0 -377
  22. fractal_server/app/routes/api/v1/__init__.py +0 -26
  23. fractal_server/app/routes/api/v1/_aux_functions.py +0 -478
  24. fractal_server/app/routes/api/v1/dataset.py +0 -554
  25. fractal_server/app/routes/api/v1/job.py +0 -195
  26. fractal_server/app/routes/api/v1/project.py +0 -475
  27. fractal_server/app/routes/api/v1/task.py +0 -203
  28. fractal_server/app/routes/api/v1/task_collection.py +0 -239
  29. fractal_server/app/routes/api/v1/workflow.py +0 -355
  30. fractal_server/app/routes/api/v1/workflowtask.py +0 -187
  31. fractal_server/app/runner/async_wrap_v1.py +0 -27
  32. fractal_server/app/runner/v1/__init__.py +0 -415
  33. fractal_server/app/runner/v1/_common.py +0 -620
  34. fractal_server/app/runner/v1/_local/__init__.py +0 -186
  35. fractal_server/app/runner/v1/_local/_local_config.py +0 -105
  36. fractal_server/app/runner/v1/_local/_submit_setup.py +0 -48
  37. fractal_server/app/runner/v1/_local/executor.py +0 -100
  38. fractal_server/app/runner/v1/_slurm/__init__.py +0 -312
  39. fractal_server/app/runner/v1/_slurm/_submit_setup.py +0 -81
  40. fractal_server/app/runner/v1/_slurm/get_slurm_config.py +0 -163
  41. fractal_server/app/runner/v1/common.py +0 -117
  42. fractal_server/app/runner/v1/handle_failed_job.py +0 -141
  43. fractal_server/app/schemas/v1/__init__.py +0 -37
  44. fractal_server/app/schemas/v1/applyworkflow.py +0 -161
  45. fractal_server/app/schemas/v1/dataset.py +0 -165
  46. fractal_server/app/schemas/v1/dumps.py +0 -64
  47. fractal_server/app/schemas/v1/manifest.py +0 -126
  48. fractal_server/app/schemas/v1/project.py +0 -66
  49. fractal_server/app/schemas/v1/state.py +0 -18
  50. fractal_server/app/schemas/v1/task.py +0 -167
  51. fractal_server/app/schemas/v1/task_collection.py +0 -110
  52. fractal_server/app/schemas/v1/workflow.py +0 -212
  53. fractal_server/tasks/v1/_TaskCollectPip.py +0 -103
  54. fractal_server/tasks/v1/__init__.py +0 -0
  55. fractal_server/tasks/v1/background_operations.py +0 -352
  56. fractal_server/tasks/v1/endpoint_operations.py +0 -156
  57. fractal_server/tasks/v1/get_collection_data.py +0 -14
  58. fractal_server/tasks/v1/utils.py +0 -67
  59. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0a0.dist-info}/LICENSE +0 -0
  60. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0a0.dist-info}/WHEEL +0 -0
  61. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0a0.dist-info}/entry_points.txt +0 -0
@@ -1,352 +0,0 @@
1
- """
2
- The main function exported from this module is `background_collect_pip`, which
3
- is used as a background task for the task-collection endpoint.
4
- """
5
- import json
6
- from pathlib import Path
7
- from shutil import rmtree as shell_rmtree
8
-
9
- from ...string_tools import slugify_task_name_for_source_v1
10
- from ..utils import get_collection_log_v1
11
- from ..utils import get_collection_path
12
- from ..utils import get_log_path
13
- from ..v2.utils_package_names import normalize_package_name
14
- from ._TaskCollectPip import _TaskCollectPip
15
- from .utils import _init_venv_v1
16
- from fractal_server.app.db import DBSyncSession
17
- from fractal_server.app.db import get_sync_db
18
- from fractal_server.app.models.v1 import State
19
- from fractal_server.app.models.v1 import Task
20
- from fractal_server.app.schemas.v1 import TaskCollectStatusV1
21
- from fractal_server.app.schemas.v1 import TaskCreateV1
22
- from fractal_server.app.schemas.v1 import TaskReadV1
23
- from fractal_server.logger import close_logger
24
- from fractal_server.logger import get_logger
25
- from fractal_server.logger import set_logger
26
- from fractal_server.utils import execute_command_async
27
-
28
-
29
- async def _pip_install(
30
- venv_path: Path,
31
- task_pkg: _TaskCollectPip,
32
- logger_name: str,
33
- ) -> Path:
34
- """
35
- Install package in venv
36
-
37
- Args:
38
- venv_path:
39
- task_pkg:
40
- logger_name:
41
-
42
- Returns:
43
- The location of the package.
44
- """
45
-
46
- logger = get_logger(logger_name)
47
-
48
- pip = venv_path / "venv/bin/pip"
49
-
50
- extras = f"[{task_pkg.package_extras}]" if task_pkg.package_extras else ""
51
-
52
- if task_pkg.is_local_package:
53
- pip_install_str = f"{task_pkg.package_path.as_posix()}{extras}"
54
- else:
55
- version_string = (
56
- f"=={task_pkg.package_version}" if task_pkg.package_version else ""
57
- )
58
- pip_install_str = f"{task_pkg.package}{extras}{version_string}"
59
-
60
- cmd_install = f"{pip} install {pip_install_str}"
61
- cmd_inspect = f"{pip} show {task_pkg.package}"
62
-
63
- await execute_command_async(
64
- cwd=venv_path,
65
- command=f"{pip} install --upgrade pip",
66
- logger_name=logger_name,
67
- )
68
- await execute_command_async(
69
- cwd=venv_path, command=cmd_install, logger_name=logger_name
70
- )
71
- if task_pkg.pinned_package_versions:
72
- for (
73
- pinned_pkg_name,
74
- pinned_pkg_version,
75
- ) in task_pkg.pinned_package_versions.items():
76
-
77
- logger.debug(
78
- "Specific version required: "
79
- f"{pinned_pkg_name}=={pinned_pkg_version}"
80
- )
81
- logger.debug(
82
- "Preliminary check: verify that "
83
- f"{pinned_pkg_version} is already installed"
84
- )
85
- stdout_inspect = await execute_command_async(
86
- cwd=venv_path,
87
- command=f"{pip} show {pinned_pkg_name}",
88
- logger_name=logger_name,
89
- )
90
- current_version = next(
91
- line.split()[-1]
92
- for line in stdout_inspect.split("\n")
93
- if line.startswith("Version:")
94
- )
95
- if current_version != pinned_pkg_version:
96
- logger.debug(
97
- f"Currently installed version of {pinned_pkg_name} "
98
- f"({current_version}) differs from pinned version "
99
- f"({pinned_pkg_version}); "
100
- f"install version {pinned_pkg_version}."
101
- )
102
- await execute_command_async(
103
- cwd=venv_path,
104
- command=(
105
- f"{pip} install "
106
- f"{pinned_pkg_name}=={pinned_pkg_version}"
107
- ),
108
- logger_name=logger_name,
109
- )
110
- else:
111
- logger.debug(
112
- f"Currently installed version of {pinned_pkg_name} "
113
- f"({current_version}) already matches the pinned version."
114
- )
115
-
116
- # Extract package installation path from `pip show`
117
- stdout_inspect = await execute_command_async(
118
- cwd=venv_path, command=cmd_inspect, logger_name=logger_name
119
- )
120
-
121
- location = Path(
122
- next(
123
- line.split()[-1]
124
- for line in stdout_inspect.split("\n")
125
- if line.startswith("Location:")
126
- )
127
- )
128
-
129
- # NOTE
130
- # https://packaging.python.org/en/latest/specifications/recording-installed-packages/
131
- # This directory is named as {name}-{version}.dist-info, with name and
132
- # version fields corresponding to Core metadata specifications. Both
133
- # fields must be normalized (see the name normalization specification and
134
- # the version normalization specification), and replace dash (-)
135
- # characters with underscore (_) characters, so the .dist-info directory
136
- # always has exactly one dash (-) character in its stem, separating the
137
- # name and version fields.
138
- package_root = location / (task_pkg.package.replace("-", "_"))
139
- logger.debug(f"[_pip install] {location=}")
140
- logger.debug(f"[_pip install] {task_pkg.package=}")
141
- logger.debug(f"[_pip install] {package_root=}")
142
- if not package_root.exists():
143
- raise RuntimeError(
144
- "Could not determine package installation location."
145
- )
146
- return package_root
147
-
148
-
149
- async def _create_venv_install_package(
150
- *,
151
- task_pkg: _TaskCollectPip,
152
- path: Path,
153
- logger_name: str,
154
- ) -> tuple[Path, Path]:
155
- """Create venv and install package
156
-
157
- Args:
158
- path: the directory in which to create the environment
159
- task_pkg: object containing the different metadata required to install
160
- the package
161
-
162
- Returns:
163
- python_bin: path to venv's python interpreter
164
- package_root: the location of the package manifest
165
- """
166
-
167
- # Normalize package name
168
- task_pkg.package_name = normalize_package_name(task_pkg.package_name)
169
- task_pkg.package = normalize_package_name(task_pkg.package)
170
-
171
- python_bin = await _init_venv_v1(
172
- path=path,
173
- python_version=task_pkg.python_version,
174
- logger_name=logger_name,
175
- )
176
- package_root = await _pip_install(
177
- venv_path=path, task_pkg=task_pkg, logger_name=logger_name
178
- )
179
- return python_bin, package_root
180
-
181
-
182
- async def create_package_environment_pip(
183
- *,
184
- task_pkg: _TaskCollectPip,
185
- venv_path: Path,
186
- logger_name: str,
187
- ) -> list[TaskCreateV1]:
188
- """
189
- Create environment, install package, and prepare task list
190
- """
191
-
192
- logger = get_logger(logger_name)
193
-
194
- # Normalize package name
195
- task_pkg.package_name = normalize_package_name(task_pkg.package_name)
196
- task_pkg.package = normalize_package_name(task_pkg.package)
197
-
198
- # Only proceed if package, version and manifest attributes are set
199
- task_pkg.check()
200
-
201
- try:
202
-
203
- logger.debug("Creating venv and installing package")
204
- python_bin, package_root = await _create_venv_install_package(
205
- path=venv_path,
206
- task_pkg=task_pkg,
207
- logger_name=logger_name,
208
- )
209
- logger.debug("Venv creation and package installation ended correctly.")
210
-
211
- # Prepare task_list with appropriate metadata
212
- logger.debug("Creating task list from manifest")
213
- task_list = []
214
- for t in task_pkg.package_manifest.task_list:
215
- # Fill in attributes for TaskCreate
216
- task_executable = package_root / t.executable
217
- cmd = f"{python_bin.as_posix()} {task_executable.as_posix()}"
218
- task_name_slug = slugify_task_name_for_source_v1(t.name)
219
- task_source = f"{task_pkg.package_source}:{task_name_slug}"
220
- if not task_executable.exists():
221
- raise FileNotFoundError(
222
- f"Cannot find executable `{task_executable}` "
223
- f"for task `{t.name}`"
224
- )
225
- manifest = task_pkg.package_manifest
226
- if manifest.has_args_schemas:
227
- additional_attrs = dict(
228
- args_schema_version=manifest.args_schema_version
229
- )
230
- else:
231
- additional_attrs = {}
232
- this_task = TaskCreateV1(
233
- **t.dict(),
234
- command=cmd,
235
- version=task_pkg.package_version,
236
- **additional_attrs,
237
- source=task_source,
238
- )
239
- task_list.append(this_task)
240
- logger.debug("Task list created correctly")
241
- except Exception as e:
242
- logger.error("Task manifest loading failed")
243
- raise e
244
- return task_list
245
-
246
-
247
- async def _insert_tasks(
248
- task_list: list[TaskCreateV1],
249
- db: DBSyncSession,
250
- ) -> list[Task]:
251
- """
252
- Insert tasks into database
253
- """
254
- task_db_list = [Task(**t.dict()) for t in task_list]
255
- db.add_all(task_db_list)
256
- db.commit()
257
- for t in task_db_list:
258
- db.refresh(t)
259
- db.close()
260
- return task_db_list
261
-
262
-
263
- async def background_collect_pip(
264
- state_id: int,
265
- venv_path: Path,
266
- task_pkg: _TaskCollectPip,
267
- ) -> None:
268
- """
269
- Install package and collect tasks
270
-
271
- Install a python package and collect the tasks it provides according to
272
- the manifest.
273
-
274
- In case of error, copy the log into the state and delete the package
275
- directory.
276
- """
277
- logger_name = task_pkg.package.replace("/", "_")
278
- logger = set_logger(
279
- logger_name=logger_name,
280
- log_file_path=get_log_path(venv_path),
281
- )
282
- logger.debug("Start background task collection")
283
- for key, value in task_pkg.dict(exclude={"package_manifest"}).items():
284
- logger.debug(f"{key}: {value}")
285
-
286
- with next(get_sync_db()) as db:
287
- state: State = db.get(State, state_id)
288
- data = TaskCollectStatusV1(**state.data)
289
- data.info = None
290
-
291
- try:
292
- # install
293
- logger.debug("Task-collection status: installing")
294
- data.status = "installing"
295
-
296
- state.data = data.sanitised_dict()
297
- db.merge(state)
298
- db.commit()
299
- task_list = await create_package_environment_pip(
300
- venv_path=venv_path,
301
- task_pkg=task_pkg,
302
- logger_name=logger_name,
303
- )
304
-
305
- # collect
306
- logger.debug("Task-collection status: collecting")
307
- data.status = "collecting"
308
- state.data = data.sanitised_dict()
309
- db.merge(state)
310
- db.commit()
311
- tasks = await _insert_tasks(task_list=task_list, db=db)
312
-
313
- # finalise
314
- logger.debug("Task-collection status: finalising")
315
- collection_path = get_collection_path(venv_path)
316
- data.task_list = [
317
- TaskReadV1(**task.model_dump()) for task in tasks
318
- ]
319
- with collection_path.open("w") as f:
320
- json.dump(data.sanitised_dict(), f, indent=2)
321
-
322
- # Update DB
323
- data.status = "OK"
324
- data.log = get_collection_log_v1(venv_path)
325
- state.data = data.sanitised_dict()
326
- db.add(state)
327
- db.merge(state)
328
- db.commit()
329
-
330
- # Write last logs to file
331
- logger.debug("Task-collection status: OK")
332
- logger.info("Background task collection completed successfully")
333
- close_logger(logger)
334
- db.close()
335
-
336
- except Exception as e:
337
- # Write last logs to file
338
- logger.debug("Task-collection status: fail")
339
- logger.info(f"Background collection failed. Original error: {e}")
340
- close_logger(logger)
341
-
342
- # Update db
343
- data.status = "fail"
344
- data.info = f"Original error: {e}"
345
- data.log = get_collection_log_v1(venv_path)
346
- state.data = data.sanitised_dict()
347
- db.merge(state)
348
- db.commit()
349
- db.close()
350
-
351
- # Delete corrupted package dir
352
- shell_rmtree(venv_path)
@@ -1,156 +0,0 @@
1
- import json
2
- from pathlib import Path
3
- from typing import Optional
4
- from typing import Union
5
- from zipfile import ZipFile
6
-
7
- from ..v2.utils_package_names import normalize_package_name
8
- from ._TaskCollectPip import _TaskCollectPip as _TaskCollectPipV1
9
- from .utils import get_python_interpreter_v1
10
- from fractal_server.app.schemas.v1 import ManifestV1
11
- from fractal_server.config import get_settings
12
- from fractal_server.logger import get_logger
13
- from fractal_server.syringe import Inject
14
- from fractal_server.utils import execute_command_async
15
-
16
-
17
- FRACTAL_PUBLIC_TASK_SUBDIR = ".fractal"
18
-
19
-
20
- async def download_package(
21
- *,
22
- task_pkg: _TaskCollectPipV1,
23
- dest: Union[str, Path],
24
- ) -> Path:
25
- """
26
- Download package to destination
27
- """
28
- interpreter = get_python_interpreter_v1(version=task_pkg.python_version)
29
- pip = f"{interpreter} -m pip"
30
- version = (
31
- f"=={task_pkg.package_version}" if task_pkg.package_version else ""
32
- )
33
- package_and_version = f"{task_pkg.package}{version}"
34
- cmd = f"{pip} download --no-deps {package_and_version} -d {dest}"
35
- stdout = await execute_command_async(command=cmd, cwd=Path("."))
36
- pkg_file = next(
37
- line.split()[-1] for line in stdout.split("\n") if "Saved" in line
38
- )
39
- return Path(pkg_file)
40
-
41
-
42
- def _load_manifest_from_wheel(
43
- path: Path, wheel: ZipFile, logger_name: Optional[str] = None
44
- ) -> ManifestV1:
45
- logger = get_logger(logger_name)
46
- namelist = wheel.namelist()
47
- try:
48
- manifest = next(
49
- name for name in namelist if "__FRACTAL_MANIFEST__.json" in name
50
- )
51
- except StopIteration:
52
- msg = f"{path.as_posix()} does not include __FRACTAL_MANIFEST__.json"
53
- logger.error(msg)
54
- raise ValueError(msg)
55
- with wheel.open(manifest) as manifest_fd:
56
- manifest_dict = json.load(manifest_fd)
57
- manifest_version = str(manifest_dict["manifest_version"])
58
- if manifest_version == "1":
59
- pkg_manifest = ManifestV1(**manifest_dict)
60
- return pkg_manifest
61
- else:
62
- msg = f"Manifest version {manifest_version=} not supported"
63
- logger.error(msg)
64
- raise ValueError(msg)
65
-
66
-
67
- def inspect_package(path: Path, logger_name: Optional[str] = None) -> dict:
68
- """
69
- Inspect task package to extract version, name and manifest
70
-
71
- Note that this only works with wheel files, which have a well-defined
72
- dist-info section. If we need to generalize to to tar.gz archives, we would
73
- need to go and look for `PKG-INFO`.
74
-
75
- Note: package name is normalized via `_normalize_package_name`.
76
-
77
- Args:
78
- path: Path
79
- the path in which the package is saved
80
-
81
- Returns:
82
- version_manifest: A dictionary containing `version`, the version of the
83
- pacakge, and `manifest`, the Fractal manifest object relative to the
84
- tasks.
85
- """
86
-
87
- logger = get_logger(logger_name)
88
-
89
- if not path.as_posix().endswith(".whl"):
90
- raise ValueError(
91
- f"Only wheel packages are supported, given {path.as_posix()}."
92
- )
93
-
94
- with ZipFile(path) as wheel:
95
- namelist = wheel.namelist()
96
-
97
- # Read and validate task manifest
98
- logger.debug(f"Now reading manifest for {path.as_posix()}")
99
- pkg_manifest = _load_manifest_from_wheel(
100
- path, wheel, logger_name=logger_name
101
- )
102
- logger.debug("Manifest read correctly.")
103
-
104
- # Read package name and version from *.dist-info/METADATA
105
- logger.debug(
106
- f"Now reading package name and version for {path.as_posix()}"
107
- )
108
- metadata = next(
109
- name for name in namelist if "dist-info/METADATA" in name
110
- )
111
- with wheel.open(metadata) as metadata_fd:
112
- meta = metadata_fd.read().decode("utf-8")
113
- pkg_name = next(
114
- line.split()[-1]
115
- for line in meta.splitlines()
116
- if line.startswith("Name")
117
- )
118
- pkg_version = next(
119
- line.split()[-1]
120
- for line in meta.splitlines()
121
- if line.startswith("Version")
122
- )
123
- logger.debug("Package name and version read correctly.")
124
-
125
- # Normalize package name:
126
- pkg_name = normalize_package_name(pkg_name)
127
-
128
- info = dict(
129
- pkg_name=pkg_name,
130
- pkg_version=pkg_version,
131
- pkg_manifest=pkg_manifest,
132
- )
133
- return info
134
-
135
-
136
- def create_package_dir_pip(
137
- *,
138
- task_pkg: _TaskCollectPipV1,
139
- create: bool = True,
140
- ) -> Path:
141
- """
142
- Create venv folder for a task package and return corresponding Path object
143
- """
144
- settings = Inject(get_settings)
145
- user = FRACTAL_PUBLIC_TASK_SUBDIR
146
- if task_pkg.package_version is None:
147
- raise ValueError(
148
- f"Cannot create venv folder for package `{task_pkg.package}` "
149
- "with `version=None`."
150
- )
151
- normalized_package = normalize_package_name(task_pkg.package)
152
- package_dir = f"{normalized_package}{task_pkg.package_version}"
153
- venv_path = settings.FRACTAL_TASKS_DIR / user / package_dir
154
- if create:
155
- venv_path.mkdir(exist_ok=False, parents=True)
156
- return venv_path
@@ -1,14 +0,0 @@
1
- import json
2
- from pathlib import Path
3
-
4
- from fractal_server.app.schemas.v1 import TaskCollectStatusV1
5
- from fractal_server.tasks.utils import get_absolute_venv_path_v1
6
- from fractal_server.tasks.utils import get_collection_path
7
-
8
-
9
- def get_collection_data(venv_path: Path) -> TaskCollectStatusV1:
10
- package_path = get_absolute_venv_path_v1(venv_path)
11
- collection_path = get_collection_path(package_path)
12
- with collection_path.open() as f:
13
- data = json.load(f)
14
- return TaskCollectStatusV1(**data)
@@ -1,67 +0,0 @@
1
- from pathlib import Path
2
- from typing import Optional
3
-
4
- from fractal_server.logger import get_logger
5
- from fractal_server.utils import execute_command_async
6
-
7
-
8
- def get_python_interpreter_v1(version: Optional[str] = None) -> str:
9
- """
10
- Return the path to the python interpreter
11
-
12
- Args:
13
- version: Python version
14
-
15
- Raises:
16
- ValueError: If the python version requested is not available on the
17
- host.
18
-
19
- Returns:
20
- interpreter: string representing the python executable or its path
21
- """
22
- import shutil
23
- import sys
24
-
25
- if version:
26
- interpreter = shutil.which(f"python{version}")
27
- if not interpreter:
28
- raise ValueError(
29
- f"Python version {version} not available on host."
30
- )
31
- else:
32
- interpreter = sys.executable
33
-
34
- return interpreter
35
-
36
-
37
- async def _init_venv_v1(
38
- *,
39
- path: Path,
40
- python_version: Optional[str] = None,
41
- logger_name: str,
42
- ) -> Path:
43
- """
44
- Set a virtual environment at `path/venv`
45
-
46
- Args:
47
- path : Path
48
- path to directory in which to set up the virtual environment
49
- python_version : default=None
50
- Python version the virtual environment will be based upon
51
-
52
- Returns:
53
- python_bin : Path
54
- path to python interpreter
55
- """
56
- logger = get_logger(logger_name)
57
- logger.debug(f"[_init_venv] {path=}")
58
- interpreter = get_python_interpreter_v1(version=python_version)
59
- logger.debug(f"[_init_venv] {interpreter=}")
60
- await execute_command_async(
61
- cwd=path,
62
- command=f"{interpreter} -m venv venv",
63
- logger_name=logger_name,
64
- )
65
- python_bin = path / "venv/bin/python"
66
- logger.debug(f"[_init_venv] {python_bin=}")
67
- return python_bin