fractal-server 2.8.1__py3-none-any.whl → 2.9.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 (51) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/__init__.py +3 -3
  3. fractal_server/app/models/v2/task.py +0 -72
  4. fractal_server/app/models/v2/task_group.py +102 -0
  5. fractal_server/app/routes/admin/v1.py +1 -20
  6. fractal_server/app/routes/admin/v2/job.py +1 -20
  7. fractal_server/app/routes/admin/v2/task_group.py +53 -13
  8. fractal_server/app/routes/api/v2/__init__.py +11 -2
  9. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +43 -0
  10. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +21 -14
  11. fractal_server/app/routes/api/v2/task_collection.py +26 -51
  12. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  13. fractal_server/app/routes/api/v2/task_group.py +83 -14
  14. fractal_server/app/routes/api/v2/task_group_lifecycle.py +221 -0
  15. fractal_server/app/routes/api/v2/workflow.py +1 -1
  16. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  17. fractal_server/app/routes/aux/_timestamp.py +25 -0
  18. fractal_server/app/schemas/v2/__init__.py +3 -2
  19. fractal_server/app/schemas/v2/task_collection.py +0 -21
  20. fractal_server/app/schemas/v2/task_group.py +30 -6
  21. fractal_server/migrations/versions/3082479ac4ea_taskgroup_activity_and_venv_info_to_.py +105 -0
  22. fractal_server/ssh/_fabric.py +18 -0
  23. fractal_server/tasks/utils.py +2 -12
  24. fractal_server/tasks/v2/local/__init__.py +3 -0
  25. fractal_server/tasks/v2/local/collect.py +291 -0
  26. fractal_server/tasks/v2/local/deactivate.py +162 -0
  27. fractal_server/tasks/v2/local/reactivate.py +159 -0
  28. fractal_server/tasks/v2/local/utils_local.py +52 -0
  29. fractal_server/tasks/v2/ssh/__init__.py +0 -0
  30. fractal_server/tasks/v2/ssh/collect.py +387 -0
  31. fractal_server/tasks/v2/ssh/deactivate.py +2 -0
  32. fractal_server/tasks/v2/ssh/reactivate.py +2 -0
  33. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  34. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  35. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  36. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  37. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  38. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  39. fractal_server/tasks/v2/utils_background.py +42 -103
  40. fractal_server/tasks/v2/utils_templates.py +32 -2
  41. fractal_server/utils.py +4 -2
  42. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a0.dist-info}/METADATA +2 -2
  43. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a0.dist-info}/RECORD +47 -36
  44. fractal_server/app/models/v2/collection_state.py +0 -22
  45. fractal_server/tasks/v2/collection_local.py +0 -357
  46. fractal_server/tasks/v2/collection_ssh.py +0 -352
  47. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  48. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  49. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a0.dist-info}/LICENSE +0 -0
  50. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a0.dist-info}/WHEEL +0 -0
  51. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0a0.dist-info}/entry_points.txt +0 -0
@@ -17,22 +17,24 @@ from .....logger import set_logger
17
17
  from .....syringe import Inject
18
18
  from ....db import AsyncSession
19
19
  from ....db import get_async_db
20
- from ....models.v2 import CollectionStateV2
21
20
  from ....models.v2 import TaskGroupV2
22
- from ....schemas.v2 import CollectionStateReadV2
23
- from ....schemas.v2 import CollectionStatusV2
24
21
  from ....schemas.v2 import TaskCollectPipV2
22
+ from ....schemas.v2 import TaskGroupActivityStatusV2
23
+ from ....schemas.v2 import TaskGroupActivityV2Read
25
24
  from ....schemas.v2 import TaskGroupCreateV2
26
25
  from ...aux.validate_user_settings import validate_user_settings
27
- from ._aux_functions_task_collection import get_package_version_from_pypi
26
+ from ._aux_functions_task_lifecycle import get_package_version_from_pypi
28
27
  from ._aux_functions_tasks import _get_valid_user_group_id
29
28
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
30
29
  from ._aux_functions_tasks import _verify_non_duplication_user_constraint
31
30
  from fractal_server.app.models import UserOAuth
32
- from fractal_server.app.routes.auth import current_active_user
31
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
33
32
  from fractal_server.app.routes.auth import current_active_verified_user
33
+ from fractal_server.app.schemas.v2 import (
34
+ TaskGroupActivityActionV2,
35
+ )
34
36
  from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
35
- from fractal_server.tasks.v2.collection_local import (
37
+ from fractal_server.tasks.v2.local.collect import (
36
38
  collect_package_local,
37
39
  )
38
40
  from fractal_server.tasks.v2.utils_package_names import _parse_wheel_filename
@@ -48,7 +50,7 @@ logger = set_logger(__name__)
48
50
 
49
51
  @router.post(
50
52
  "/collect/pip/",
51
- response_model=CollectionStateReadV2,
53
+ response_model=TaskGroupActivityV2Read,
52
54
  )
53
55
  async def collect_tasks_pip(
54
56
  task_collect: TaskCollectPipV2,
@@ -59,7 +61,7 @@ async def collect_tasks_pip(
59
61
  user_group_id: Optional[int] = None,
60
62
  user: UserOAuth = Depends(current_active_verified_user),
61
63
  db: AsyncSession = Depends(get_async_db),
62
- ) -> CollectionStateReadV2:
64
+ ) -> TaskGroupActivityV2Read:
63
65
  """
64
66
  Task collection endpoint
65
67
 
@@ -227,20 +229,17 @@ async def collect_tasks_pip(
227
229
  db.expunge(task_group)
228
230
 
229
231
  # All checks are OK, proceed with task collection
230
- collection_state_data = dict(
231
- status=CollectionStatusV2.PENDING,
232
- package=task_group.pkg_name,
232
+ task_group_activity = TaskGroupActivityV2(
233
+ user_id=task_group.user_id,
234
+ taskgroupv2_id=task_group.id,
235
+ status=TaskGroupActivityStatusV2.PENDING,
236
+ action=TaskGroupActivityActionV2.COLLECT,
237
+ pkg_name=task_group.pkg_name,
233
238
  version=task_group.version,
234
- path=task_group.path,
235
- venv_path=task_group.venv_path,
236
- )
237
- state = CollectionStateV2(
238
- data=collection_state_data, taskgroupv2_id=task_group.id
239
239
  )
240
- db.add(state)
240
+ db.add(task_group_activity)
241
241
  await db.commit()
242
- await db.refresh(state)
243
-
242
+ await db.refresh(task_group_activity)
244
243
  logger = set_logger(logger_name="collect_tasks_pip")
245
244
 
246
245
  # END of SSH/non-SSH common part
@@ -248,7 +247,7 @@ async def collect_tasks_pip(
248
247
  if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
249
248
  # SSH task collection
250
249
 
251
- from fractal_server.tasks.v2.collection_ssh import (
250
+ from fractal_server.tasks.v2.ssh.collect import (
252
251
  collect_package_ssh,
253
252
  )
254
253
 
@@ -263,8 +262,8 @@ async def collect_tasks_pip(
263
262
 
264
263
  background_tasks.add_task(
265
264
  collect_package_ssh,
266
- state_id=state.id,
267
- task_group=task_group,
265
+ task_group_id=task_group.id,
266
+ task_group_activity_id=task_group_activity.id,
268
267
  fractal_ssh=fractal_ssh,
269
268
  tasks_base_dir=user_settings.ssh_tasks_dir,
270
269
  )
@@ -273,37 +272,13 @@ async def collect_tasks_pip(
273
272
  # Local task collection
274
273
  background_tasks.add_task(
275
274
  collect_package_local,
276
- state_id=state.id,
277
- task_group=task_group,
275
+ task_group_id=task_group.id,
276
+ task_group_activity_id=task_group_activity.id,
278
277
  )
279
278
  logger.debug(
280
279
  "Task-collection endpoint: start background collection "
281
- "and return state"
280
+ "and return task_group_activity"
282
281
  )
283
282
  reset_logger_handlers(logger)
284
- info = (
285
- "Collecting tasks in the background. "
286
- f"GET /task/collect/{state.id}/ to query collection status"
287
- )
288
- state.data["info"] = info
289
- response.status_code = status.HTTP_201_CREATED
290
-
291
- return state
292
-
293
-
294
- @router.get("/collect/{state_id}/", response_model=CollectionStateReadV2)
295
- async def check_collection_status(
296
- state_id: int,
297
- user: UserOAuth = Depends(current_active_user),
298
- db: AsyncSession = Depends(get_async_db),
299
- ) -> CollectionStateReadV2: # State[TaskCollectStatus]
300
- """
301
- Check status of background task collection
302
- """
303
- state = await db.get(CollectionStateV2, state_id)
304
- if state is None:
305
- raise HTTPException(
306
- status_code=status.HTTP_404_NOT_FOUND,
307
- detail=f"No task collection info with id={state_id}",
308
- )
309
- return state
283
+ response.status_code = status.HTTP_202_ACCEPTED
284
+ return task_group_activity
@@ -27,12 +27,12 @@ from fractal_server.config import get_settings
27
27
  from fractal_server.logger import set_logger
28
28
  from fractal_server.string_tools import validate_cmd
29
29
  from fractal_server.syringe import Inject
30
- from fractal_server.tasks.v2.database_operations import (
31
- create_db_tasks_and_update_task_group,
32
- )
33
30
  from fractal_server.tasks.v2.utils_background import (
34
31
  _prepare_tasks_metadata,
35
32
  )
33
+ from fractal_server.tasks.v2.utils_database import (
34
+ create_db_tasks_and_update_task_group,
35
+ )
36
36
 
37
37
  router = APIRouter()
38
38
 
@@ -1,3 +1,6 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
1
4
  from fastapi import APIRouter
2
5
  from fastapi import Depends
3
6
  from fastapi import HTTPException
@@ -13,13 +16,16 @@ from fractal_server.app.db import AsyncSession
13
16
  from fractal_server.app.db import get_async_db
14
17
  from fractal_server.app.models import LinkUserGroup
15
18
  from fractal_server.app.models import UserOAuth
16
- from fractal_server.app.models.v2 import CollectionStateV2
19
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
17
20
  from fractal_server.app.models.v2 import TaskGroupV2
18
21
  from fractal_server.app.models.v2 import WorkflowTaskV2
19
22
  from fractal_server.app.routes.auth import current_active_user
20
23
  from fractal_server.app.routes.auth._aux_auth import (
21
24
  _verify_user_belongs_to_group,
22
25
  )
26
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
27
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
28
+ from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
23
29
  from fractal_server.app.schemas.v2 import TaskGroupReadV2
24
30
  from fractal_server.app.schemas.v2 import TaskGroupUpdateV2
25
31
  from fractal_server.logger import set_logger
@@ -29,6 +35,70 @@ router = APIRouter()
29
35
  logger = set_logger(__name__)
30
36
 
31
37
 
38
+ @router.get("/activity/", response_model=list[TaskGroupActivityV2Read])
39
+ async def get_task_group_activity_list(
40
+ task_group_activity_id: Optional[int] = None,
41
+ taskgroupv2_id: Optional[int] = None,
42
+ pkg_name: Optional[str] = None,
43
+ status: Optional[TaskGroupActivityStatusV2] = None,
44
+ action: Optional[TaskGroupActivityActionV2] = None,
45
+ timestamp_started_min: Optional[datetime] = None,
46
+ user: UserOAuth = Depends(current_active_user),
47
+ db: AsyncSession = Depends(get_async_db),
48
+ ) -> list[TaskGroupActivityV2Read]:
49
+
50
+ stm = select(TaskGroupActivityV2).where(
51
+ TaskGroupActivityV2.user_id == user.id
52
+ )
53
+ if task_group_activity_id is not None:
54
+ stm = stm.where(TaskGroupActivityV2.id == task_group_activity_id)
55
+ if taskgroupv2_id is not None:
56
+ stm = stm.where(TaskGroupActivityV2.taskgroupv2_id == taskgroupv2_id)
57
+ if pkg_name is not None:
58
+ stm = stm.where(TaskGroupActivityV2.pkg_name.icontains(pkg_name))
59
+ if status is not None:
60
+ stm = stm.where(TaskGroupActivityV2.status == status)
61
+ if action is not None:
62
+ stm = stm.where(TaskGroupActivityV2.action == action)
63
+ if timestamp_started_min is not None:
64
+ stm = stm.where(
65
+ TaskGroupActivityV2.timestamp_started >= timestamp_started_min
66
+ )
67
+
68
+ res = await db.execute(stm)
69
+ activities = res.scalars().all()
70
+ return activities
71
+
72
+
73
+ @router.get(
74
+ "/activity/{task_group_activity_id}/",
75
+ response_model=TaskGroupActivityV2Read,
76
+ )
77
+ async def get_task_group_activity(
78
+ task_group_activity_id: int,
79
+ user: UserOAuth = Depends(current_active_user),
80
+ db: AsyncSession = Depends(get_async_db),
81
+ ) -> TaskGroupActivityV2Read:
82
+
83
+ activity = await db.get(TaskGroupActivityV2, task_group_activity_id)
84
+
85
+ if activity is None:
86
+ raise HTTPException(
87
+ status_code=status.HTTP_404_NOT_FOUND,
88
+ detail=f"TaskGroupActivityV2 {task_group_activity_id} not found",
89
+ )
90
+ if activity.user_id != user.id:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_403_FORBIDDEN,
93
+ detail=(
94
+ "You are not the owner of TaskGroupActivityV2 "
95
+ f"{task_group_activity_id}",
96
+ ),
97
+ )
98
+
99
+ return activity
100
+
101
+
32
102
  @router.get("/", response_model=list[TaskGroupReadV2])
33
103
  async def get_task_group_list(
34
104
  user: UserOAuth = Depends(current_active_user),
@@ -40,7 +110,6 @@ async def get_task_group_list(
40
110
  """
41
111
  Get all accessible TaskGroups
42
112
  """
43
-
44
113
  if only_owner:
45
114
  condition = TaskGroupV2.user_id == user.id
46
115
  else:
@@ -112,22 +181,22 @@ async def delete_task_group(
112
181
  detail=f"TaskV2 {workflow_tasks[0].task_id} is still in use",
113
182
  )
114
183
 
115
- # Cascade operations: set foreign-keys to null for CollectionStateV2 which
116
- # are in relationship with the current TaskGroupV2
117
- logger.debug("Start of cascade operations on CollectionStateV2.")
118
- stm = select(CollectionStateV2).where(
119
- CollectionStateV2.taskgroupv2_id == task_group_id
184
+ # Cascade operations: set foreign-keys to null for TaskGroupActivityV2
185
+ # which are in relationship with the current TaskGroupV2
186
+ logger.debug("Start of cascade operations on TaskGroupActivityV2.")
187
+ stm = select(TaskGroupActivityV2).where(
188
+ TaskGroupActivityV2.taskgroupv2_id == task_group_id
120
189
  )
121
190
  res = await db.execute(stm)
122
- collection_states = res.scalars().all()
123
- for collection_state in collection_states:
191
+ task_group_activity_list = res.scalars().all()
192
+ for task_group_activity in task_group_activity_list:
124
193
  logger.debug(
125
- f"Setting CollectionStateV2[{collection_state.id}].taskgroupv2_id "
126
- "to None."
194
+ f"Setting TaskGroupActivityV2[{task_group_activity.id}]"
195
+ ".taskgroupv2_id to None."
127
196
  )
128
- collection_state.taskgroupv2_id = None
129
- db.add(collection_state)
130
- logger.debug("End of cascade operations on CollectionStateV2.")
197
+ task_group_activity.taskgroupv2_id = None
198
+ db.add(task_group_activity)
199
+ logger.debug("End of cascade operations on TaskGroupActivityV2.")
131
200
 
132
201
  await db.delete(task_group)
133
202
  await db.commit()
@@ -0,0 +1,221 @@
1
+ from fastapi import APIRouter
2
+ from fastapi import BackgroundTasks
3
+ from fastapi import Depends
4
+ from fastapi import HTTPException
5
+ from fastapi import Response
6
+ from fastapi import status
7
+
8
+ from ._aux_functions_task_lifecycle import check_no_ongoing_activity
9
+ from ._aux_functions_tasks import _get_task_group_full_access
10
+ from fractal_server.app.db import AsyncSession
11
+ from fractal_server.app.db import get_async_db
12
+ from fractal_server.app.models import UserOAuth
13
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
14
+ from fractal_server.app.routes.auth import current_active_user
15
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
16
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
17
+ from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
18
+ from fractal_server.app.schemas.v2 import TaskGroupReadV2
19
+ from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
20
+ from fractal_server.config import get_settings
21
+ from fractal_server.logger import set_logger
22
+ from fractal_server.syringe import Inject
23
+ from fractal_server.tasks.v2.local import deactivate_local
24
+ from fractal_server.tasks.v2.local import reactivate_local
25
+ from fractal_server.utils import get_timestamp
26
+
27
+ router = APIRouter()
28
+
29
+
30
+ logger = set_logger(__name__)
31
+
32
+
33
+ @router.post(
34
+ "/{task_group_id}/deactivate/",
35
+ response_model=TaskGroupActivityV2Read,
36
+ )
37
+ async def deactivate_task_group(
38
+ task_group_id: int,
39
+ background_tasks: BackgroundTasks,
40
+ response: Response,
41
+ user: UserOAuth = Depends(current_active_user),
42
+ db: AsyncSession = Depends(get_async_db),
43
+ ) -> TaskGroupReadV2:
44
+ """
45
+ Deactivate task-group venv
46
+ """
47
+ # Check access
48
+ task_group = await _get_task_group_full_access(
49
+ task_group_id=task_group_id,
50
+ user_id=user.id,
51
+ db=db,
52
+ )
53
+
54
+ # Check no other activity is ongoing
55
+ await check_no_ongoing_activity(task_group_id=task_group_id, db=db)
56
+
57
+ # Check that task-group is active
58
+ if not task_group.active:
59
+ raise HTTPException(
60
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
61
+ detail=(
62
+ f"Cannot deactivate a task group with {task_group.active=}."
63
+ ),
64
+ )
65
+
66
+ # Shortcut for task-group with origin="other"
67
+ if task_group.origin == TaskGroupV2OriginEnum.OTHER:
68
+ task_group.active = False
69
+ task_group_activity = TaskGroupActivityV2(
70
+ user_id=task_group.user_id,
71
+ taskgroupv2_id=task_group.id,
72
+ status=TaskGroupActivityStatusV2.OK,
73
+ action=TaskGroupActivityActionV2.DEACTIVATE,
74
+ pkg_name=task_group.pkg_name,
75
+ version=(task_group.version or "N/A"),
76
+ log=(
77
+ f"Task group has {task_group.origin=}, set "
78
+ "task_group.active to False and exit."
79
+ ),
80
+ timestamp_started=get_timestamp(),
81
+ timestamp_ended=get_timestamp(),
82
+ )
83
+ db.add(task_group)
84
+ db.add(task_group_activity)
85
+ await db.commit()
86
+ response.status_code = status.HTTP_202_ACCEPTED
87
+ return task_group_activity
88
+
89
+ task_group_activity = TaskGroupActivityV2(
90
+ user_id=task_group.user_id,
91
+ taskgroupv2_id=task_group.id,
92
+ status=TaskGroupActivityStatusV2.PENDING,
93
+ action=TaskGroupActivityActionV2.DEACTIVATE,
94
+ pkg_name=task_group.pkg_name,
95
+ version=task_group.version,
96
+ timestamp_started=get_timestamp(),
97
+ )
98
+ task_group.active = False
99
+ db.add(task_group)
100
+ db.add(task_group_activity)
101
+ await db.commit()
102
+
103
+ # Submit background task
104
+ settings = Inject(get_settings)
105
+ if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
106
+ raise HTTPException(
107
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
108
+ detail="Not implemented (yet) for SSH.",
109
+ )
110
+ else:
111
+ background_tasks.add_task(
112
+ deactivate_local,
113
+ task_group_id=task_group.id,
114
+ task_group_activity_id=task_group_activity.id,
115
+ )
116
+
117
+ logger.debug(
118
+ "Task group deactivation endpoint: start deactivate "
119
+ "and return task_group_activity"
120
+ )
121
+ response.status_code = status.HTTP_202_ACCEPTED
122
+ return task_group_activity
123
+
124
+
125
+ @router.post(
126
+ "/{task_group_id}/reactivate/",
127
+ response_model=TaskGroupActivityV2Read,
128
+ )
129
+ async def reactivate_task_group(
130
+ task_group_id: int,
131
+ background_tasks: BackgroundTasks,
132
+ response: Response,
133
+ user: UserOAuth = Depends(current_active_user),
134
+ db: AsyncSession = Depends(get_async_db),
135
+ ) -> TaskGroupReadV2:
136
+ """
137
+ Deactivate task-group venv
138
+ """
139
+
140
+ # Check access
141
+ task_group = await _get_task_group_full_access(
142
+ task_group_id=task_group_id,
143
+ user_id=user.id,
144
+ db=db,
145
+ )
146
+
147
+ # Check that task-group is not active
148
+ if task_group.active:
149
+ raise HTTPException(
150
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
151
+ detail=(
152
+ f"Cannot reactivate a task group with {task_group.active=}."
153
+ ),
154
+ )
155
+
156
+ # Check no other activity is ongoing
157
+ await check_no_ongoing_activity(task_group_id=task_group_id, db=db)
158
+
159
+ # Shortcut for task-group with origin="other"
160
+ if task_group.origin == TaskGroupV2OriginEnum.OTHER:
161
+ task_group.active = True
162
+ task_group_activity = TaskGroupActivityV2(
163
+ user_id=task_group.user_id,
164
+ taskgroupv2_id=task_group.id,
165
+ status=TaskGroupActivityStatusV2.OK,
166
+ action=TaskGroupActivityActionV2.REACTIVATE,
167
+ pkg_name=task_group.pkg_name,
168
+ version=(task_group.version or "N/A"),
169
+ log=(
170
+ f"Task group has {task_group.origin=}, set "
171
+ "task_group.active to True and exit."
172
+ ),
173
+ timestamp_started=get_timestamp(),
174
+ timestamp_ended=get_timestamp(),
175
+ )
176
+ db.add(task_group)
177
+ db.add(task_group_activity)
178
+ await db.commit()
179
+ response.status_code = status.HTTP_202_ACCEPTED
180
+ return task_group_activity
181
+
182
+ if task_group.pip_freeze is None:
183
+ raise HTTPException(
184
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
185
+ detail=(
186
+ "Cannot reactivate a task group with "
187
+ f"{task_group.pip_freeze=}."
188
+ ),
189
+ )
190
+
191
+ task_group_activity = TaskGroupActivityV2(
192
+ user_id=task_group.user_id,
193
+ taskgroupv2_id=task_group.id,
194
+ status=TaskGroupActivityStatusV2.PENDING,
195
+ action=TaskGroupActivityActionV2.REACTIVATE,
196
+ pkg_name=task_group.pkg_name,
197
+ version=task_group.version,
198
+ timestamp_started=get_timestamp(),
199
+ )
200
+ db.add(task_group_activity)
201
+ await db.commit()
202
+
203
+ # Submit background task
204
+ settings = Inject(get_settings)
205
+ if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
206
+ raise HTTPException(
207
+ status_code=status.HTTP_501_NOT_IMPLEMENTED,
208
+ detail="Not implemented (yet) for SSH.",
209
+ )
210
+ else:
211
+ background_tasks.add_task(
212
+ reactivate_local,
213
+ task_group_id=task_group.id,
214
+ task_group_activity_id=task_group_activity.id,
215
+ )
216
+ logger.debug(
217
+ "Task group reactivation endpoint: start reactivate "
218
+ "and return task_group_activity"
219
+ )
220
+ response.status_code = status.HTTP_202_ACCEPTED
221
+ return task_group_activity
@@ -23,7 +23,7 @@ from ._aux_functions import _get_submitted_jobs_statement
23
23
  from ._aux_functions import _get_workflow_check_owner
24
24
  from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
25
25
  from fractal_server.app.models import UserOAuth
26
- from fractal_server.app.models.v2.task import TaskGroupV2
26
+ from fractal_server.app.models.v2 import TaskGroupV2
27
27
  from fractal_server.app.routes.auth import current_active_user
28
28
 
29
29
  router = APIRouter()
@@ -21,10 +21,10 @@ from ._aux_functions import _workflow_insert_task
21
21
  from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
22
22
  from fractal_server.app.models import LinkUserGroup
23
23
  from fractal_server.app.models import UserOAuth
24
- from fractal_server.app.models.v2.task import TaskGroupV2
24
+ from fractal_server.app.models.v2 import TaskGroupV2
25
25
  from fractal_server.app.routes.auth import current_active_user
26
26
  from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
27
- from fractal_server.app.schemas.v2.task import TaskImportV2
27
+ from fractal_server.app.schemas.v2 import TaskImportV2
28
28
  from fractal_server.logger import set_logger
29
29
 
30
30
  router = APIRouter()
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+ from datetime import timezone
3
+
4
+ from fastapi import HTTPException
5
+ from fastapi import status
6
+
7
+ from fractal_server.config import get_settings
8
+ from fractal_server.syringe import Inject
9
+
10
+
11
+ def _convert_to_db_timestamp(dt: datetime) -> datetime:
12
+ """
13
+ This function takes a timezone-aware datetime and converts it to UTC.
14
+ If using SQLite, it also removes the timezone information in order to make
15
+ the datetime comparable with datetimes in the database.
16
+ """
17
+ if dt.tzinfo is None:
18
+ raise HTTPException(
19
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
20
+ detail=f"The timestamp provided has no timezone information: {dt}",
21
+ )
22
+ _dt = dt.astimezone(timezone.utc)
23
+ if Inject(get_settings).DB_ENGINE == "sqlite":
24
+ return _dt.replace(tzinfo=None)
25
+ return _dt
@@ -23,10 +23,11 @@ from .task import TaskImportV2 # noqa F401
23
23
  from .task import TaskImportV2Legacy # noqa F401
24
24
  from .task import TaskReadV2 # noqa F401
25
25
  from .task import TaskUpdateV2 # noqa F401
26
- from .task_collection import CollectionStateReadV2 # noqa F401
27
- from .task_collection import CollectionStatusV2 # noqa F401
28
26
  from .task_collection import TaskCollectCustomV2 # noqa F401
29
27
  from .task_collection import TaskCollectPipV2 # noqa F401
28
+ from .task_group import TaskGroupActivityActionV2 # noqa F401
29
+ from .task_group import TaskGroupActivityStatusV2 # noqa F401
30
+ from .task_group import TaskGroupActivityV2Read # noqa F401
30
31
  from .task_group import TaskGroupCreateV2 # noqa F401
31
32
  from .task_group import TaskGroupReadV2 # noqa F401
32
33
  from .task_group import TaskGroupUpdateV2 # noqa F401
@@ -1,7 +1,4 @@
1
- from datetime import datetime
2
- from enum import Enum
3
1
  from pathlib import Path
4
- from typing import Any
5
2
  from typing import Literal
6
3
  from typing import Optional
7
4
 
@@ -11,19 +8,10 @@ from pydantic import root_validator
11
8
  from pydantic import validator
12
9
 
13
10
  from .._validators import valstr
14
- from fractal_server.app.schemas._validators import valutc
15
11
  from fractal_server.app.schemas.v2 import ManifestV2
16
12
  from fractal_server.string_tools import validate_cmd
17
13
 
18
14
 
19
- class CollectionStatusV2(str, Enum):
20
- PENDING = "pending"
21
- INSTALLING = "installing"
22
- COLLECTING = "collecting"
23
- FAIL = "fail"
24
- OK = "OK"
25
-
26
-
27
15
  class TaskCollectPipV2(BaseModel, extra=Extra.forbid):
28
16
  """
29
17
  TaskCollectPipV2 class
@@ -191,12 +179,3 @@ class TaskCollectCustomV2(BaseModel, extra=Extra.forbid):
191
179
  f"Python interpreter path must be absolute: (given {value})."
192
180
  )
193
181
  return value
194
-
195
-
196
- class CollectionStateReadV2(BaseModel):
197
-
198
- id: Optional[int]
199
- data: dict[str, Any]
200
- timestamp: datetime
201
-
202
- _timestamp = validator("timestamp", allow_reuse=True)(valutc("timestamp"))
@@ -20,6 +20,19 @@ class TaskGroupV2OriginEnum(str, Enum):
20
20
  OTHER = "other"
21
21
 
22
22
 
23
+ class TaskGroupActivityStatusV2(str, Enum):
24
+ PENDING = "pending"
25
+ ONGOING = "ongoing"
26
+ FAILED = "failed"
27
+ OK = "OK"
28
+
29
+
30
+ class TaskGroupActivityActionV2(str, Enum):
31
+ COLLECT = "collect"
32
+ DEACTIVATE = "deactivate"
33
+ REACTIVATE = "reactivate"
34
+
35
+
23
36
  class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
24
37
  user_id: int
25
38
  user_group_id: Optional[int] = None
@@ -32,6 +45,7 @@ class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
32
45
  venv_path: Optional[str] = None
33
46
  wheel_path: Optional[str] = None
34
47
  pip_extras: Optional[str] = None
48
+ pip_freeze: Optional[str] = None
35
49
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
36
50
 
37
51
  # Validators
@@ -67,19 +81,29 @@ class TaskGroupReadV2(BaseModel):
67
81
  path: Optional[str] = None
68
82
  venv_path: Optional[str] = None
69
83
  wheel_path: Optional[str] = None
84
+ pip_freeze: Optional[str] = None
70
85
  pip_extras: Optional[str] = None
71
86
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
72
87
 
88
+ venv_size_in_kB: Optional[int] = None
89
+ venv_file_number: Optional[int] = None
90
+
73
91
  active: bool
74
92
  timestamp_created: datetime
75
93
 
76
94
 
77
95
  class TaskGroupUpdateV2(BaseModel, extra=Extra.forbid):
78
96
  user_group_id: Optional[int] = None
79
- active: Optional[bool] = None
80
97
 
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
98
+
99
+ class TaskGroupActivityV2Read(BaseModel):
100
+ id: int
101
+ user_id: int
102
+ taskgroupv2_id: Optional[int] = None
103
+ timestamp_started: datetime
104
+ timestamp_ended: Optional[datetime] = None
105
+ pkg_name: str
106
+ version: str
107
+ status: TaskGroupActivityStatusV2
108
+ action: TaskGroupActivityActionV2
109
+ log: Optional[str] = None