fractal-server 2.11.1__py3-none-any.whl → 2.12.0__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 (64) 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/executors/slurm/ssh/executor.py +9 -6
  6. fractal_server/app/runner/executors/slurm/sudo/executor.py +1 -5
  7. fractal_server/app/runner/filenames.py +0 -2
  8. fractal_server/app/runner/shutdown.py +3 -27
  9. fractal_server/app/schemas/_validators.py +0 -19
  10. fractal_server/config.py +1 -15
  11. fractal_server/main.py +1 -12
  12. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +67 -0
  13. fractal_server/string_tools.py +0 -21
  14. fractal_server/tasks/utils.py +0 -28
  15. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0.dist-info}/METADATA +1 -1
  16. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0.dist-info}/RECORD +19 -63
  17. fractal_server/app/models/v1/__init__.py +0 -13
  18. fractal_server/app/models/v1/dataset.py +0 -71
  19. fractal_server/app/models/v1/job.py +0 -101
  20. fractal_server/app/models/v1/project.py +0 -29
  21. fractal_server/app/models/v1/state.py +0 -34
  22. fractal_server/app/models/v1/task.py +0 -85
  23. fractal_server/app/models/v1/workflow.py +0 -133
  24. fractal_server/app/routes/admin/v1.py +0 -377
  25. fractal_server/app/routes/api/v1/__init__.py +0 -26
  26. fractal_server/app/routes/api/v1/_aux_functions.py +0 -478
  27. fractal_server/app/routes/api/v1/dataset.py +0 -554
  28. fractal_server/app/routes/api/v1/job.py +0 -195
  29. fractal_server/app/routes/api/v1/project.py +0 -475
  30. fractal_server/app/routes/api/v1/task.py +0 -203
  31. fractal_server/app/routes/api/v1/task_collection.py +0 -239
  32. fractal_server/app/routes/api/v1/workflow.py +0 -355
  33. fractal_server/app/routes/api/v1/workflowtask.py +0 -187
  34. fractal_server/app/runner/async_wrap_v1.py +0 -27
  35. fractal_server/app/runner/v1/__init__.py +0 -415
  36. fractal_server/app/runner/v1/_common.py +0 -620
  37. fractal_server/app/runner/v1/_local/__init__.py +0 -186
  38. fractal_server/app/runner/v1/_local/_local_config.py +0 -105
  39. fractal_server/app/runner/v1/_local/_submit_setup.py +0 -48
  40. fractal_server/app/runner/v1/_local/executor.py +0 -100
  41. fractal_server/app/runner/v1/_slurm/__init__.py +0 -312
  42. fractal_server/app/runner/v1/_slurm/_submit_setup.py +0 -81
  43. fractal_server/app/runner/v1/_slurm/get_slurm_config.py +0 -163
  44. fractal_server/app/runner/v1/common.py +0 -117
  45. fractal_server/app/runner/v1/handle_failed_job.py +0 -141
  46. fractal_server/app/schemas/v1/__init__.py +0 -37
  47. fractal_server/app/schemas/v1/applyworkflow.py +0 -161
  48. fractal_server/app/schemas/v1/dataset.py +0 -165
  49. fractal_server/app/schemas/v1/dumps.py +0 -64
  50. fractal_server/app/schemas/v1/manifest.py +0 -126
  51. fractal_server/app/schemas/v1/project.py +0 -66
  52. fractal_server/app/schemas/v1/state.py +0 -18
  53. fractal_server/app/schemas/v1/task.py +0 -167
  54. fractal_server/app/schemas/v1/task_collection.py +0 -110
  55. fractal_server/app/schemas/v1/workflow.py +0 -212
  56. fractal_server/tasks/v1/_TaskCollectPip.py +0 -103
  57. fractal_server/tasks/v1/__init__.py +0 -0
  58. fractal_server/tasks/v1/background_operations.py +0 -352
  59. fractal_server/tasks/v1/endpoint_operations.py +0 -156
  60. fractal_server/tasks/v1/get_collection_data.py +0 -14
  61. fractal_server/tasks/v1/utils.py +0 -67
  62. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0.dist-info}/LICENSE +0 -0
  63. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0.dist-info}/WHEEL +0 -0
  64. {fractal_server-2.11.1.dist-info → fractal_server-2.12.0.dist-info}/entry_points.txt +0 -0
@@ -1,203 +0,0 @@
1
- from copy import deepcopy # noqa
2
- from typing import Optional
3
-
4
- from fastapi import APIRouter
5
- from fastapi import Depends
6
- from fastapi import HTTPException
7
- from fastapi import Response
8
- from fastapi import status
9
- from sqlmodel import select
10
-
11
- from .....logger import set_logger
12
- from ....db import AsyncSession
13
- from ....db import get_async_db
14
- from ....models.v1 import Task
15
- from ....models.v1 import WorkflowTask
16
- from ....models.v2 import TaskV2
17
- from ....schemas.v1 import TaskCreateV1
18
- from ....schemas.v1 import TaskReadV1
19
- from ....schemas.v1 import TaskUpdateV1
20
- from ...aux.validate_user_settings import verify_user_has_settings
21
- from ._aux_functions import _get_task_check_owner
22
- from ._aux_functions import _raise_if_v1_is_read_only
23
- from fractal_server.app.models import UserOAuth
24
- from fractal_server.app.routes.auth import current_active_user
25
- from fractal_server.app.routes.auth import current_active_verified_user
26
-
27
- router = APIRouter()
28
-
29
- logger = set_logger(__name__)
30
-
31
-
32
- @router.get("/", response_model=list[TaskReadV1])
33
- async def get_list_task(
34
- user: UserOAuth = Depends(current_active_user),
35
- args_schema: bool = True,
36
- db: AsyncSession = Depends(get_async_db),
37
- ) -> list[TaskReadV1]:
38
- """
39
- Get list of available tasks
40
- """
41
- stm = select(Task)
42
- res = await db.execute(stm)
43
- task_list = res.scalars().all()
44
- await db.close()
45
- if not args_schema:
46
- for task in task_list:
47
- setattr(task, "args_schema", None)
48
-
49
- return task_list
50
-
51
-
52
- @router.get("/{task_id}/", response_model=TaskReadV1)
53
- async def get_task(
54
- task_id: int,
55
- user: UserOAuth = Depends(current_active_user),
56
- db: AsyncSession = Depends(get_async_db),
57
- ) -> TaskReadV1:
58
- """
59
- Get info on a specific task
60
- """
61
- task = await db.get(Task, task_id)
62
- await db.close()
63
- if not task:
64
- raise HTTPException(
65
- status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
66
- )
67
- return task
68
-
69
-
70
- @router.patch("/{task_id}/", response_model=TaskReadV1)
71
- async def patch_task(
72
- task_id: int,
73
- task_update: TaskUpdateV1,
74
- user: UserOAuth = Depends(current_active_verified_user),
75
- db: AsyncSession = Depends(get_async_db),
76
- ) -> Optional[TaskReadV1]:
77
- """
78
- Edit a specific task (restricted to superusers and task owner)
79
- """
80
- _raise_if_v1_is_read_only()
81
- if task_update.source:
82
- raise HTTPException(
83
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
84
- detail="patch_task endpoint cannot set `source`",
85
- )
86
-
87
- # Retrieve task from database
88
- db_task = await _get_task_check_owner(task_id=task_id, user=user, db=db)
89
-
90
- update = task_update.dict(exclude_unset=True)
91
- for key, value in update.items():
92
- if isinstance(value, str) or (
93
- key == "version" and value is None
94
- ): # special case (issue 817)
95
- setattr(db_task, key, value)
96
- elif isinstance(value, dict):
97
- if key == "args_schema":
98
- setattr(db_task, key, value)
99
- else:
100
- current_dict = deepcopy(getattr(db_task, key))
101
- current_dict.update(value)
102
- setattr(db_task, key, current_dict)
103
- else:
104
- raise HTTPException(
105
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
106
- detail=f"Invalid {type(value)=} for {key=}",
107
- )
108
-
109
- await db.commit()
110
- await db.refresh(db_task)
111
- await db.close()
112
- return db_task
113
-
114
-
115
- @router.post(
116
- "/", response_model=TaskReadV1, status_code=status.HTTP_201_CREATED
117
- )
118
- async def create_task(
119
- task: TaskCreateV1,
120
- user: UserOAuth = Depends(current_active_verified_user),
121
- db: AsyncSession = Depends(get_async_db),
122
- ) -> Optional[TaskReadV1]:
123
- """
124
- Create a new task
125
- """
126
- _raise_if_v1_is_read_only()
127
- # Set task.owner attribute
128
- if user.username:
129
- owner = user.username
130
- else:
131
- verify_user_has_settings(user)
132
- if user.settings.slurm_user:
133
- owner = user.settings.slurm_user
134
- else:
135
- raise HTTPException(
136
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
137
- detail=(
138
- "Cannot add a new task because current user does not "
139
- "have `username` or `slurm_user` attributes."
140
- ),
141
- )
142
-
143
- # Prepend owner to task.source
144
- task.source = f"{owner}:{task.source}"
145
-
146
- # Verify that source is not already in use (note: this check is only useful
147
- # to provide a user-friendly error message, but `task.source` uniqueness is
148
- # already guaranteed by a constraint in the table definition).
149
- stm = select(Task).where(Task.source == task.source)
150
- res = await db.execute(stm)
151
- if res.scalars().all():
152
- raise HTTPException(
153
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
154
- detail=f"Source '{task.source}' already used by some TaskV1",
155
- )
156
- stm = select(TaskV2).where(TaskV2.source == task.source)
157
- res = await db.execute(stm)
158
- if res.scalars().all():
159
- raise HTTPException(
160
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
161
- detail=f"Source '{task.source}' already used by some TaskV2",
162
- )
163
-
164
- # Add task
165
- db_task = Task(**task.dict(), owner=owner)
166
- db.add(db_task)
167
- await db.commit()
168
- await db.refresh(db_task)
169
- await db.close()
170
- return db_task
171
-
172
-
173
- @router.delete("/{task_id}/", status_code=204)
174
- async def delete_task(
175
- task_id: int,
176
- user: UserOAuth = Depends(current_active_user),
177
- db: AsyncSession = Depends(get_async_db),
178
- ) -> Response:
179
- """
180
- Delete a task
181
- """
182
- _raise_if_v1_is_read_only()
183
- db_task = await _get_task_check_owner(task_id=task_id, user=user, db=db)
184
-
185
- # Check that the Task is not in relationship with some WorkflowTask
186
- stm = select(WorkflowTask).filter(WorkflowTask.task_id == task_id)
187
- res = await db.execute(stm)
188
- workflowtask_list = res.scalars().all()
189
- if workflowtask_list:
190
- raise HTTPException(
191
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
192
- detail=(
193
- f"Cannot remove Task {task_id} because it is currently "
194
- "imported in Workflows "
195
- f"{[x.workflow_id for x in workflowtask_list]}. "
196
- "If you want to remove this task, then you should first remove"
197
- " the workflows.",
198
- ),
199
- )
200
-
201
- await db.delete(db_task)
202
- await db.commit()
203
- return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -1,239 +0,0 @@
1
- from pathlib import Path
2
- from shutil import copy as shell_copy
3
- from tempfile import TemporaryDirectory
4
-
5
- from fastapi import APIRouter
6
- from fastapi import BackgroundTasks
7
- from fastapi import Depends
8
- from fastapi import HTTPException
9
- from fastapi import Response
10
- from fastapi import status
11
- from pydantic.error_wrappers import ValidationError
12
- from sqlmodel import select
13
-
14
- from .....config import get_settings
15
- from .....logger import close_logger
16
- from .....logger import set_logger
17
- from .....syringe import Inject
18
- from ....db import AsyncSession
19
- from ....db import get_async_db
20
- from ....models.v1 import State
21
- from ....models.v1 import Task
22
- from ....schemas.v1 import StateRead
23
- from ....schemas.v1 import TaskCollectPipV1
24
- from ....schemas.v1 import TaskCollectStatusV1
25
- from ._aux_functions import _raise_if_v1_is_read_only
26
- from fractal_server.app.models import UserOAuth
27
- from fractal_server.app.routes.auth import current_active_user
28
- from fractal_server.app.routes.auth import current_active_verified_user
29
- from fractal_server.string_tools import slugify_task_name_for_source_v1
30
- from fractal_server.tasks.utils import get_collection_log_v1
31
- from fractal_server.tasks.v1._TaskCollectPip import _TaskCollectPip
32
- from fractal_server.tasks.v1.background_operations import (
33
- background_collect_pip,
34
- )
35
- from fractal_server.tasks.v1.endpoint_operations import create_package_dir_pip
36
- from fractal_server.tasks.v1.endpoint_operations import download_package
37
- from fractal_server.tasks.v1.endpoint_operations import inspect_package
38
- from fractal_server.tasks.v1.get_collection_data import get_collection_data
39
-
40
- router = APIRouter()
41
-
42
- logger = set_logger(__name__)
43
-
44
-
45
- @router.post(
46
- "/collect/pip/",
47
- response_model=StateRead,
48
- responses={
49
- 201: dict(
50
- description=(
51
- "Task collection successfully started in the background"
52
- )
53
- ),
54
- 200: dict(
55
- description=(
56
- "Package already collected. Returning info on already "
57
- "available tasks"
58
- )
59
- ),
60
- },
61
- )
62
- async def collect_tasks_pip(
63
- task_collect: TaskCollectPipV1,
64
- background_tasks: BackgroundTasks,
65
- response: Response,
66
- user: UserOAuth = Depends(current_active_verified_user),
67
- db: AsyncSession = Depends(get_async_db),
68
- ) -> StateRead: # State[TaskCollectStatus]
69
- """
70
- Task collection endpoint
71
-
72
- Trigger the creation of a dedicated virtual environment, the installation
73
- of a package and the collection of tasks as advertised in the manifest.
74
- """
75
- _raise_if_v1_is_read_only()
76
- logger = set_logger(logger_name="collect_tasks_pip")
77
-
78
- # Validate payload as _TaskCollectPip, which has more strict checks than
79
- # TaskCollectPip
80
- try:
81
- task_pkg = _TaskCollectPip(**task_collect.dict(exclude_unset=True))
82
- except ValidationError as e:
83
- raise HTTPException(
84
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
85
- detail=f"Invalid task-collection object. Original error: {e}",
86
- )
87
-
88
- with TemporaryDirectory() as tmpdir:
89
- try:
90
- # Copy or download the package wheel file to tmpdir
91
- if task_pkg.is_local_package:
92
- shell_copy(task_pkg.package_path.as_posix(), tmpdir)
93
- pkg_path = Path(tmpdir) / task_pkg.package_path.name
94
- else:
95
- pkg_path = await download_package(
96
- task_pkg=task_pkg, dest=tmpdir
97
- )
98
- # Read package info from wheel file, and override the ones coming
99
- # from the request body
100
- pkg_info = inspect_package(pkg_path)
101
- task_pkg.package_name = pkg_info["pkg_name"]
102
- task_pkg.package_version = pkg_info["pkg_version"]
103
- task_pkg.package_manifest = pkg_info["pkg_manifest"]
104
- task_pkg.check()
105
- except Exception as e:
106
- raise HTTPException(
107
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
108
- detail=f"Invalid package or manifest. Original error: {e}",
109
- )
110
-
111
- try:
112
- venv_path = create_package_dir_pip(task_pkg=task_pkg)
113
- except FileExistsError:
114
- venv_path = create_package_dir_pip(task_pkg=task_pkg, create=False)
115
- try:
116
- task_collect_status = get_collection_data(venv_path)
117
- for task in task_collect_status.task_list:
118
- db_task = await db.get(Task, task.id)
119
- if (
120
- (not db_task)
121
- or db_task.source != task.source
122
- or db_task.name != task.name
123
- ):
124
- await db.close()
125
- raise HTTPException(
126
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
127
- detail=(
128
- "Cannot collect package. Folder already exists, "
129
- f"but task {task.id} does not exists or it does "
130
- f"not have the expected source ({task.source}) or "
131
- f"name ({task.name})."
132
- ),
133
- )
134
- except FileNotFoundError as e:
135
- await db.close()
136
- raise HTTPException(
137
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
138
- detail=(
139
- "Cannot collect package. Possible reason: another "
140
- "collection of the same package is in progress. "
141
- f"Original error: {e}"
142
- ),
143
- )
144
- except ValidationError as e:
145
- await db.close()
146
- raise HTTPException(
147
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
148
- detail=(
149
- "Cannot collect package. Possible reason: an old version "
150
- "of the same package has already been collected. "
151
- f"Original error: {e}"
152
- ),
153
- )
154
- task_collect_status.info = "Already installed"
155
- state = State(data=task_collect_status.sanitised_dict())
156
- response.status_code == status.HTTP_200_OK
157
- await db.close()
158
- return state
159
- settings = Inject(get_settings)
160
-
161
- # Check that tasks are not already in the DB
162
- for new_task in task_pkg.package_manifest.task_list:
163
- new_task_name_slug = slugify_task_name_for_source_v1(new_task.name)
164
- new_task_source = f"{task_pkg.package_source}:{new_task_name_slug}"
165
- stm = select(Task).where(Task.source == new_task_source)
166
- res = await db.execute(stm)
167
- if res.scalars().all():
168
- raise HTTPException(
169
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
170
- detail=(
171
- "Cannot collect package. Task with source "
172
- f'"{new_task_source}" already exists in the database.'
173
- ),
174
- )
175
-
176
- # All checks are OK, proceed with task collection
177
- full_venv_path = venv_path.relative_to(settings.FRACTAL_TASKS_DIR)
178
- collection_status = TaskCollectStatusV1(
179
- status="pending", venv_path=full_venv_path, package=task_pkg.package
180
- )
181
-
182
- # Create State object (after casting venv_path to string)
183
- collection_status_dict = collection_status.dict()
184
- collection_status_dict["venv_path"] = str(collection_status.venv_path)
185
- state = State(data=collection_status_dict)
186
- db.add(state)
187
- await db.commit()
188
- await db.refresh(state)
189
-
190
- background_tasks.add_task(
191
- background_collect_pip,
192
- state_id=state.id,
193
- venv_path=venv_path,
194
- task_pkg=task_pkg,
195
- )
196
- logger.debug(
197
- "Task-collection endpoint: start background collection "
198
- "and return state"
199
- )
200
- close_logger(logger)
201
- info = (
202
- "Collecting tasks in the background. "
203
- f"GET /task/collect/{state.id} to query collection status"
204
- )
205
- state.data["info"] = info
206
- response.status_code = status.HTTP_201_CREATED
207
- await db.close()
208
- return state
209
-
210
-
211
- @router.get("/collect/{state_id}/", response_model=StateRead)
212
- async def check_collection_status(
213
- state_id: int,
214
- user: UserOAuth = Depends(current_active_user),
215
- verbose: bool = False,
216
- db: AsyncSession = Depends(get_async_db),
217
- ) -> StateRead: # State[TaskCollectStatus]
218
- """
219
- Check status of background task collection
220
- """
221
- logger = set_logger(logger_name="check_collection_status")
222
- logger.debug(f"Querying state for state.id={state_id}")
223
- state = await db.get(State, state_id)
224
- if not state:
225
- await db.close()
226
- raise HTTPException(
227
- status_code=status.HTTP_404_NOT_FOUND,
228
- detail=f"No task collection info with id={state_id}",
229
- )
230
- data = TaskCollectStatusV1(**state.data)
231
-
232
- # In some cases (i.e. a successful or ongoing task collection), data.log is
233
- # not set; if so, we collect the current logs
234
- if verbose and not data.log:
235
- data.log = get_collection_log_v1(data.venv_path)
236
- state.data = data.sanitised_dict()
237
- close_logger(logger)
238
- await db.close()
239
- return state