fractal-server 2.8.1__py3-none-any.whl → 2.9.0a1__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 (54) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -35
  3. fractal_server/app/models/v2/__init__.py +3 -3
  4. fractal_server/app/models/v2/task.py +0 -72
  5. fractal_server/app/models/v2/task_group.py +102 -0
  6. fractal_server/app/routes/admin/v1.py +1 -20
  7. fractal_server/app/routes/admin/v2/job.py +1 -20
  8. fractal_server/app/routes/admin/v2/task_group.py +53 -13
  9. fractal_server/app/routes/api/v2/__init__.py +11 -2
  10. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +43 -0
  11. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
  12. fractal_server/app/routes/api/v2/task_collection.py +30 -55
  13. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  14. fractal_server/app/routes/api/v2/task_group.py +83 -14
  15. fractal_server/app/routes/api/v2/task_group_lifecycle.py +221 -0
  16. fractal_server/app/routes/api/v2/workflow.py +1 -1
  17. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  18. fractal_server/app/routes/aux/_timestamp.py +18 -0
  19. fractal_server/app/schemas/_validators.py +1 -2
  20. fractal_server/app/schemas/v2/__init__.py +3 -2
  21. fractal_server/app/schemas/v2/task_collection.py +0 -21
  22. fractal_server/app/schemas/v2/task_group.py +31 -8
  23. fractal_server/config.py +11 -56
  24. fractal_server/migrations/versions/3082479ac4ea_taskgroup_activity_and_venv_info_to_.py +105 -0
  25. fractal_server/ssh/_fabric.py +18 -0
  26. fractal_server/tasks/utils.py +2 -12
  27. fractal_server/tasks/v2/local/__init__.py +3 -0
  28. fractal_server/tasks/v2/local/collect.py +291 -0
  29. fractal_server/tasks/v2/local/deactivate.py +210 -0
  30. fractal_server/tasks/v2/local/reactivate.py +159 -0
  31. fractal_server/tasks/v2/local/utils_local.py +45 -0
  32. fractal_server/tasks/v2/ssh/__init__.py +0 -0
  33. fractal_server/tasks/v2/ssh/collect.py +386 -0
  34. fractal_server/tasks/v2/ssh/deactivate.py +2 -0
  35. fractal_server/tasks/v2/ssh/reactivate.py +2 -0
  36. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  37. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  38. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  39. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  40. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  41. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  42. fractal_server/tasks/v2/utils_background.py +42 -103
  43. fractal_server/tasks/v2/utils_templates.py +32 -2
  44. fractal_server/utils.py +4 -2
  45. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a1.dist-info}/METADATA +2 -3
  46. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a1.dist-info}/RECORD +50 -39
  47. fractal_server/app/models/v2/collection_state.py +0 -22
  48. fractal_server/tasks/v2/collection_local.py +0 -357
  49. fractal_server/tasks/v2/collection_ssh.py +0 -352
  50. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  51. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  52. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a1.dist-info}/LICENSE +0 -0
  53. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a1.dist-info}/WHEEL +0 -0
  54. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
- from typing import Literal
4
3
  from typing import Optional
5
4
 
6
5
  from pydantic import BaseModel
@@ -20,6 +19,19 @@ class TaskGroupV2OriginEnum(str, Enum):
20
19
  OTHER = "other"
21
20
 
22
21
 
22
+ class TaskGroupActivityStatusV2(str, Enum):
23
+ PENDING = "pending"
24
+ ONGOING = "ongoing"
25
+ FAILED = "failed"
26
+ OK = "OK"
27
+
28
+
29
+ class TaskGroupActivityActionV2(str, Enum):
30
+ COLLECT = "collect"
31
+ DEACTIVATE = "deactivate"
32
+ REACTIVATE = "reactivate"
33
+
34
+
23
35
  class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
24
36
  user_id: int
25
37
  user_group_id: Optional[int] = None
@@ -32,6 +44,7 @@ class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
32
44
  venv_path: Optional[str] = None
33
45
  wheel_path: Optional[str] = None
34
46
  pip_extras: Optional[str] = None
47
+ pip_freeze: Optional[str] = None
35
48
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
36
49
 
37
50
  # Validators
@@ -60,26 +73,36 @@ class TaskGroupReadV2(BaseModel):
60
73
  user_id: int
61
74
  user_group_id: Optional[int] = None
62
75
 
63
- origin: Literal["pypi", "wheel-file", "other"]
76
+ origin: TaskGroupV2OriginEnum
64
77
  pkg_name: str
65
78
  version: Optional[str] = None
66
79
  python_version: Optional[str] = None
67
80
  path: Optional[str] = None
68
81
  venv_path: Optional[str] = None
69
82
  wheel_path: Optional[str] = None
83
+ pip_freeze: Optional[str] = None
70
84
  pip_extras: Optional[str] = None
71
85
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
72
86
 
87
+ venv_size_in_kB: Optional[int] = None
88
+ venv_file_number: Optional[int] = None
89
+
73
90
  active: bool
74
91
  timestamp_created: datetime
75
92
 
76
93
 
77
94
  class TaskGroupUpdateV2(BaseModel, extra=Extra.forbid):
78
95
  user_group_id: Optional[int] = None
79
- active: Optional[bool] = None
80
96
 
81
- @validator("active")
82
- def active_cannot_be_None(cls, value):
83
- if value is None:
84
- raise ValueError("`active` cannot be set to None")
85
- return value
97
+
98
+ class TaskGroupActivityV2Read(BaseModel):
99
+ id: int
100
+ user_id: int
101
+ taskgroupv2_id: Optional[int] = None
102
+ timestamp_started: datetime
103
+ timestamp_ended: Optional[datetime] = None
104
+ pkg_name: str
105
+ version: str
106
+ status: TaskGroupActivityStatusV2
107
+ action: TaskGroupActivityActionV2
108
+ log: Optional[str] = None
fractal_server/config.py CHANGED
@@ -16,7 +16,6 @@ import shutil
16
16
  import sys
17
17
  from os import environ
18
18
  from os import getenv
19
- from os.path import abspath
20
19
  from pathlib import Path
21
20
  from typing import Literal
22
21
  from typing import Optional
@@ -167,10 +166,6 @@ class Settings(BaseSettings):
167
166
  ###########################################################################
168
167
  # DATABASE
169
168
  ###########################################################################
170
- DB_ENGINE: Literal["sqlite", "postgres-psycopg"] = "sqlite"
171
- """
172
- Database engine to use (supported: `sqlite`, `postgres-psycopg`).
173
- """
174
169
  DB_ECHO: bool = False
175
170
  """
176
171
  If `True`, make database operations verbose.
@@ -196,44 +191,21 @@ class Settings(BaseSettings):
196
191
  Name of the PostgreSQL database to connect to.
197
192
  """
198
193
 
199
- SQLITE_PATH: Optional[str]
200
- """
201
- File path where the SQLite database is located (or will be located).
202
- """
203
-
204
194
  @property
205
195
  def DATABASE_ASYNC_URL(self) -> URL:
206
- if self.DB_ENGINE == "postgres-psycopg":
207
- url = URL.create(
208
- drivername="postgresql+psycopg",
209
- username=self.POSTGRES_USER,
210
- password=self.POSTGRES_PASSWORD,
211
- host=self.POSTGRES_HOST,
212
- port=self.POSTGRES_PORT,
213
- database=self.POSTGRES_DB,
214
- )
215
- else:
216
- if not self.SQLITE_PATH:
217
- raise FractalConfigurationError(
218
- "SQLITE_PATH path cannot be None"
219
- )
220
- sqlite_path = abspath(self.SQLITE_PATH)
221
- url = URL.create(
222
- drivername="sqlite+aiosqlite",
223
- database=sqlite_path,
224
- )
196
+ url = URL.create(
197
+ drivername="postgresql+psycopg",
198
+ username=self.POSTGRES_USER,
199
+ password=self.POSTGRES_PASSWORD,
200
+ host=self.POSTGRES_HOST,
201
+ port=self.POSTGRES_PORT,
202
+ database=self.POSTGRES_DB,
203
+ )
225
204
  return url
226
205
 
227
206
  @property
228
207
  def DATABASE_SYNC_URL(self):
229
- if self.DB_ENGINE == "postgres-psycopg":
230
- return self.DATABASE_ASYNC_URL.set(drivername="postgresql+psycopg")
231
- else:
232
- if not self.SQLITE_PATH:
233
- raise FractalConfigurationError(
234
- "SQLITE_PATH path cannot be None"
235
- )
236
- return self.DATABASE_ASYNC_URL.set(drivername="sqlite")
208
+ return self.DATABASE_ASYNC_URL.set(drivername="postgresql+psycopg")
237
209
 
238
210
  ###########################################################################
239
211
  # FRACTAL SPECIFIC
@@ -533,25 +505,8 @@ class Settings(BaseSettings):
533
505
  """
534
506
  Checks that db environment variables are properly set.
535
507
  """
536
- if self.DB_ENGINE == "postgres-psycopg":
537
- if not self.POSTGRES_DB:
538
- raise FractalConfigurationError(
539
- "POSTGRES_DB cannot be None when DB_ENGINE="
540
- "postgres-psycopg."
541
- )
542
-
543
- try:
544
- import psycopg # noqa: F401
545
- except ModuleNotFoundError:
546
- raise FractalConfigurationError(
547
- "DB engine is `postgres-psycopg` but `psycopg` is not "
548
- "available"
549
- )
550
- else:
551
- if not self.SQLITE_PATH:
552
- raise FractalConfigurationError(
553
- "SQLITE_PATH cannot be None when DB_ENGINE=sqlite."
554
- )
508
+ if not self.POSTGRES_DB:
509
+ raise FractalConfigurationError("POSTGRES_DB cannot be None.")
555
510
 
556
511
  def check_runner(self) -> None:
557
512
 
@@ -0,0 +1,105 @@
1
+ """TaskGroup Activity and venv-info to TaskGroup
2
+
3
+ Revision ID: 3082479ac4ea
4
+ Revises: 19eca0dd47a9
5
+ Create Date: 2024-11-12 14:39:34.035859
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ import sqlmodel
10
+ from alembic import op
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "3082479ac4ea"
14
+ down_revision = "19eca0dd47a9"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ op.create_table(
22
+ "taskgroupactivityv2",
23
+ sa.Column("id", sa.Integer(), nullable=False),
24
+ sa.Column("user_id", sa.Integer(), nullable=False),
25
+ sa.Column("taskgroupv2_id", sa.Integer(), nullable=True),
26
+ sa.Column(
27
+ "timestamp_started", sa.DateTime(timezone=True), nullable=False
28
+ ),
29
+ sa.Column(
30
+ "pkg_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False
31
+ ),
32
+ sa.Column(
33
+ "version", sqlmodel.sql.sqltypes.AutoString(), nullable=False
34
+ ),
35
+ sa.Column(
36
+ "status", sqlmodel.sql.sqltypes.AutoString(), nullable=False
37
+ ),
38
+ sa.Column(
39
+ "action", sqlmodel.sql.sqltypes.AutoString(), nullable=False
40
+ ),
41
+ sa.Column("log", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
42
+ sa.Column(
43
+ "timestamp_ended", sa.DateTime(timezone=True), nullable=True
44
+ ),
45
+ sa.ForeignKeyConstraint(
46
+ ["taskgroupv2_id"],
47
+ ["taskgroupv2.id"],
48
+ name=op.f("fk_taskgroupactivityv2_taskgroupv2_id_taskgroupv2"),
49
+ ),
50
+ sa.ForeignKeyConstraint(
51
+ ["user_id"],
52
+ ["user_oauth.id"],
53
+ name=op.f("fk_taskgroupactivityv2_user_id_user_oauth"),
54
+ ),
55
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_taskgroupactivityv2")),
56
+ )
57
+ op.drop_table("collectionstatev2")
58
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
59
+ batch_op.add_column(
60
+ sa.Column(
61
+ "pip_freeze", sqlmodel.sql.sqltypes.AutoString(), nullable=True
62
+ )
63
+ )
64
+ batch_op.add_column(
65
+ sa.Column("venv_size_in_kB", sa.Integer(), nullable=True)
66
+ )
67
+ batch_op.add_column(
68
+ sa.Column("venv_file_number", sa.Integer(), nullable=True)
69
+ )
70
+
71
+ # ### end Alembic commands ###
72
+
73
+
74
+ def downgrade() -> None:
75
+ # ### commands auto generated by Alembic - please adjust! ###
76
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
77
+ batch_op.drop_column("venv_file_number")
78
+ batch_op.drop_column("venv_size_in_kB")
79
+ batch_op.drop_column("pip_freeze")
80
+
81
+ op.create_table(
82
+ "collectionstatev2",
83
+ sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
84
+ sa.Column(
85
+ "data",
86
+ sa.JSON(),
87
+ nullable=True,
88
+ ),
89
+ sa.Column(
90
+ "timestamp",
91
+ sa.DateTime(timezone=True),
92
+ nullable=True,
93
+ ),
94
+ sa.Column(
95
+ "taskgroupv2_id", sa.INTEGER(), autoincrement=False, nullable=True
96
+ ),
97
+ sa.ForeignKeyConstraint(
98
+ ["taskgroupv2_id"],
99
+ ["taskgroupv2.id"],
100
+ name="fk_collectionstatev2_taskgroupv2_id_taskgroupv2",
101
+ ),
102
+ sa.PrimaryKeyConstraint("id", name="pk_collectionstatev2"),
103
+ )
104
+ op.drop_table("taskgroupactivityv2")
105
+ # ### end Alembic commands ###
@@ -471,6 +471,24 @@ class FractalSSH(object):
471
471
  f.write(content)
472
472
  self.logger.info(f"END writing to remote file {path}.")
473
473
 
474
+ def remote_exists(self, path: str) -> bool:
475
+ """
476
+ Return whether a remote file/folder exists
477
+ """
478
+ self.logger.info(f"START remote_file_exists {path}")
479
+ with _acquire_lock_with_timeout(
480
+ lock=self._lock,
481
+ label=f"remote_file_exists {path=}",
482
+ timeout=self.default_lock_timeout,
483
+ ):
484
+ try:
485
+ self._sftp_unsafe().stat(path)
486
+ self.logger.info(f"END remote_file_exists {path} / True")
487
+ return True
488
+ except FileNotFoundError:
489
+ self.logger.info(f"END remote_file_exists {path} / False")
490
+ return False
491
+
474
492
 
475
493
  class FractalSSHList(object):
476
494
  """
@@ -30,19 +30,9 @@ def get_log_path(base: Path) -> Path:
30
30
  return base / COLLECTION_LOG_FILENAME
31
31
 
32
32
 
33
- def get_freeze_path(base: Path) -> Path:
34
- return base / COLLECTION_FREEZE_FILENAME
35
-
36
-
37
33
  def get_collection_log_v1(path: Path) -> str:
38
34
  package_path = get_absolute_venv_path_v1(path)
39
35
  log_path = get_log_path(package_path)
40
- log = log_path.open().read()
36
+ with log_path.open("r") as f:
37
+ log = f.read()
41
38
  return log
42
-
43
-
44
- def get_collection_freeze_v1(venv_path: Path) -> str:
45
- package_path = get_absolute_venv_path_v1(venv_path)
46
- freeze_path = get_freeze_path(package_path)
47
- freeze = freeze_path.open().read()
48
- return freeze
@@ -0,0 +1,3 @@
1
+ from .collect import collect_local # noqa
2
+ from .deactivate import deactivate_local # noqa
3
+ from .reactivate import reactivate_local # noqa
@@ -0,0 +1,291 @@
1
+ import json
2
+ import logging
3
+ import shutil
4
+ import time
5
+ from pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+
8
+ from ..utils_database import create_db_tasks_and_update_task_group
9
+ from .utils_local import _customize_and_run_template
10
+ from fractal_server.app.db import get_sync_db
11
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
12
+ from fractal_server.app.models.v2 import TaskGroupV2
13
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
15
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
16
+ from fractal_server.logger import get_logger
17
+ from fractal_server.logger import set_logger
18
+ from fractal_server.tasks.utils import get_log_path
19
+ from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
20
+ from fractal_server.tasks.v2.utils_background import add_commit_refresh
21
+ from fractal_server.tasks.v2.utils_background import check_task_files_exist
22
+ from fractal_server.tasks.v2.utils_background import fail_and_cleanup
23
+ from fractal_server.tasks.v2.utils_background import get_current_log
24
+ from fractal_server.tasks.v2.utils_package_names import compare_package_names
25
+ from fractal_server.tasks.v2.utils_python_interpreter import (
26
+ get_python_interpreter_v2,
27
+ )
28
+ from fractal_server.tasks.v2.utils_templates import get_collection_replacements
29
+ from fractal_server.tasks.v2.utils_templates import (
30
+ parse_script_pip_show_stdout,
31
+ )
32
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
33
+ from fractal_server.utils import get_timestamp
34
+
35
+ LOGGER_NAME = __name__
36
+
37
+
38
+ def _copy_wheel_file_local(task_group: TaskGroupV2) -> str:
39
+ logger = get_logger(LOGGER_NAME)
40
+ source = task_group.wheel_path
41
+ dest = (
42
+ Path(task_group.path) / Path(task_group.wheel_path).name
43
+ ).as_posix()
44
+ logger.debug(f"[_copy_wheel_file] START {source=} {dest=}")
45
+ shutil.copy(task_group.wheel_path, task_group.path)
46
+ logger.debug(f"[_copy_wheel_file] END {source=} {dest=}")
47
+ return dest
48
+
49
+
50
+ def collect_local(
51
+ *,
52
+ task_group_activity_id: int,
53
+ task_group_id: int,
54
+ ) -> None:
55
+ """
56
+ Collect a task package.
57
+
58
+ This function is run as a background task, therefore exceptions must be
59
+ handled.
60
+
61
+ NOTE: by making this function sync, it runs within a thread - due to
62
+ starlette/fastapi handling of background tasks (see
63
+ https://github.com/encode/starlette/blob/master/starlette/background.py).
64
+
65
+
66
+ Arguments:
67
+ task_group_id:
68
+ task_group_activity_id:
69
+ """
70
+
71
+ with TemporaryDirectory() as tmpdir:
72
+ log_file_path = get_log_path(Path(tmpdir))
73
+ logger = set_logger(
74
+ logger_name=LOGGER_NAME,
75
+ log_file_path=log_file_path,
76
+ )
77
+
78
+ with next(get_sync_db()) as db:
79
+
80
+ # Get main objects from db
81
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
82
+ task_group = db.get(TaskGroupV2, task_group_id)
83
+ if activity is None or task_group is None:
84
+ # Use `logging` directly
85
+ logging.error(
86
+ "Cannot find database rows with "
87
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
88
+ f"{task_group=}\n{activity=}. Exit."
89
+ )
90
+ return
91
+
92
+ # Log some info
93
+ logger.debug("START")
94
+ for key, value in task_group.model_dump().items():
95
+ logger.debug(f"task_group.{key}: {value}")
96
+
97
+ # Check that the (local) task_group path does exist
98
+ if Path(task_group.path).exists():
99
+ error_msg = f"{task_group.path} already exists."
100
+ logger.error(error_msg)
101
+ fail_and_cleanup(
102
+ task_group=task_group,
103
+ task_group_activity=activity,
104
+ logger_name=LOGGER_NAME,
105
+ log_file_path=log_file_path,
106
+ exception=FileExistsError(error_msg),
107
+ db=db,
108
+ )
109
+ return
110
+
111
+ try:
112
+
113
+ # Create task_group.path folder
114
+ Path(task_group.path).mkdir(parents=True)
115
+ logger.debug(f"Created {task_group.path}")
116
+
117
+ # Copy wheel file into task group path
118
+ if task_group.wheel_path:
119
+ new_wheel_path = _copy_wheel_file_local(
120
+ task_group=task_group
121
+ )
122
+ task_group.wheel_path = new_wheel_path
123
+ task_group = add_commit_refresh(obj=task_group, db=db)
124
+
125
+ # Prepare replacements for templates
126
+ replacements = get_collection_replacements(
127
+ task_group=task_group,
128
+ python_bin=get_python_interpreter_v2(
129
+ python_version=task_group.python_version
130
+ ),
131
+ )
132
+
133
+ # Prepare common arguments for `_customize_and_run_template``
134
+ common_args = dict(
135
+ replacements=replacements,
136
+ script_dir=(
137
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
138
+ ).as_posix(),
139
+ prefix=(
140
+ f"{int(time.time())}_"
141
+ f"{TaskGroupActivityActionV2.COLLECT}_"
142
+ ),
143
+ logger_name=LOGGER_NAME,
144
+ )
145
+
146
+ # Set status to ONGOING and refresh logs
147
+ activity.status = TaskGroupActivityStatusV2.ONGOING
148
+ activity.log = get_current_log(log_file_path)
149
+ activity = add_commit_refresh(obj=activity, db=db)
150
+
151
+ # Run script 1
152
+ stdout = _customize_and_run_template(
153
+ template_filename="1_create_venv.sh",
154
+ **common_args,
155
+ )
156
+ activity.log = get_current_log(log_file_path)
157
+ activity = add_commit_refresh(obj=activity, db=db)
158
+
159
+ # Run script 2
160
+ stdout = _customize_and_run_template(
161
+ template_filename="2_pip_install.sh",
162
+ **common_args,
163
+ )
164
+ activity.log = get_current_log(log_file_path)
165
+ activity = add_commit_refresh(obj=activity, db=db)
166
+
167
+ # Run script 3
168
+ pip_freeze_stdout = _customize_and_run_template(
169
+ template_filename="3_pip_freeze.sh",
170
+ **common_args,
171
+ )
172
+ activity.log = get_current_log(log_file_path)
173
+ activity = add_commit_refresh(obj=activity, db=db)
174
+
175
+ # Run script 4
176
+ stdout = _customize_and_run_template(
177
+ template_filename="4_pip_show.sh",
178
+ **common_args,
179
+ )
180
+ activity.log = get_current_log(log_file_path)
181
+ activity = add_commit_refresh(obj=activity, db=db)
182
+
183
+ # Run script 5
184
+ venv_info = _customize_and_run_template(
185
+ template_filename="5_get_venv_size_and_file_number.sh",
186
+ **common_args,
187
+ )
188
+ venv_size, venv_file_number = venv_info.split()
189
+ activity.log = get_current_log(log_file_path)
190
+ activity = add_commit_refresh(obj=activity, db=db)
191
+
192
+ pkg_attrs = parse_script_pip_show_stdout(stdout)
193
+ for key, value in pkg_attrs.items():
194
+ logger.debug(f"Parsed from pip-show: {key}={value}")
195
+ # Check package_name match between pip show and task-group
196
+ task_group = db.get(TaskGroupV2, task_group_id)
197
+ package_name_pip_show = pkg_attrs.get("package_name")
198
+ package_name_task_group = task_group.pkg_name
199
+ compare_package_names(
200
+ pkg_name_pip_show=package_name_pip_show,
201
+ pkg_name_task_group=package_name_task_group,
202
+ logger_name=LOGGER_NAME,
203
+ )
204
+ # Extract/drop parsed attributes
205
+ package_name = package_name_task_group
206
+ python_bin = pkg_attrs.pop("python_bin")
207
+ package_root_parent = pkg_attrs.pop("package_root_parent")
208
+
209
+ # TODO : Use more robust logic to determine `package_root`.
210
+ # Examples: use `importlib.util.find_spec`, or parse the
211
+ # output of `pip show --files {package_name}`.
212
+ package_name_underscore = package_name.replace("-", "_")
213
+ package_root = (
214
+ Path(package_root_parent) / package_name_underscore
215
+ ).as_posix()
216
+
217
+ # Read and validate manifest file
218
+ manifest_path = pkg_attrs.pop("manifest_path")
219
+ logger.info(f"now loading {manifest_path=}")
220
+ with open(manifest_path) as json_data:
221
+ pkg_manifest_dict = json.load(json_data)
222
+ logger.info(f"loaded {manifest_path=}")
223
+ logger.info("now validating manifest content")
224
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
225
+ logger.info("validated manifest content")
226
+ activity.log = get_current_log(log_file_path)
227
+ activity = add_commit_refresh(obj=activity, db=db)
228
+
229
+ logger.info("_prepare_tasks_metadata - start")
230
+ task_list = _prepare_tasks_metadata(
231
+ package_manifest=pkg_manifest,
232
+ package_version=task_group.version,
233
+ package_root=Path(package_root),
234
+ python_bin=Path(python_bin),
235
+ )
236
+ check_task_files_exist(task_list=task_list)
237
+ logger.info("_prepare_tasks_metadata - end")
238
+ activity.log = get_current_log(log_file_path)
239
+ activity = add_commit_refresh(obj=activity, db=db)
240
+
241
+ logger.info("create_db_tasks_and_update_task_group - " "start")
242
+ create_db_tasks_and_update_task_group(
243
+ task_list=task_list,
244
+ task_group_id=task_group.id,
245
+ db=db,
246
+ )
247
+ logger.info("create_db_tasks_and_update_task_group - end")
248
+
249
+ # Update task_group data
250
+ logger.info(
251
+ "Add pip_freeze, venv_size and venv_file_number "
252
+ "to TaskGroupV2 - start"
253
+ )
254
+ task_group.pip_freeze = pip_freeze_stdout
255
+ task_group.venv_size_in_kB = int(venv_size)
256
+ task_group.venv_file_number = int(venv_file_number)
257
+ task_group = add_commit_refresh(obj=task_group, db=db)
258
+ logger.info(
259
+ "Add pip_freeze, venv_size and venv_file_number "
260
+ "to TaskGroupV2 - end"
261
+ )
262
+
263
+ # Finalize (write metadata to DB)
264
+ logger.debug("finalising - START")
265
+ activity.status = TaskGroupActivityStatusV2.OK
266
+ activity.timestamp_ended = get_timestamp()
267
+ activity = add_commit_refresh(obj=activity, db=db)
268
+ logger.debug("finalising - END")
269
+ logger.debug("END")
270
+
271
+ except Exception as collection_e:
272
+ # Delete corrupted package dir
273
+ try:
274
+ logger.info(f"Now delete folder {task_group.path}")
275
+ shutil.rmtree(task_group.path)
276
+ logger.info(f"Deleted folder {task_group.path}")
277
+ except Exception as rm_e:
278
+ logger.error(
279
+ "Removing folder failed.\n"
280
+ f"Original error:\n{str(rm_e)}"
281
+ )
282
+
283
+ fail_and_cleanup(
284
+ task_group=task_group,
285
+ task_group_activity=activity,
286
+ logger_name=LOGGER_NAME,
287
+ log_file_path=log_file_path,
288
+ exception=collection_e,
289
+ db=db,
290
+ )
291
+ return