fractal-server 2.11.0a10__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 (65) 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/models/v2/dataset.py +0 -4
  5. fractal_server/app/models/v2/workflowtask.py +0 -4
  6. fractal_server/app/routes/aux/_job.py +1 -3
  7. fractal_server/app/runner/filenames.py +0 -2
  8. fractal_server/app/runner/shutdown.py +3 -27
  9. fractal_server/config.py +1 -15
  10. fractal_server/main.py +1 -12
  11. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +67 -0
  12. fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +54 -0
  13. fractal_server/string_tools.py +0 -21
  14. fractal_server/tasks/utils.py +0 -24
  15. {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0a0.dist-info}/METADATA +1 -1
  16. {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0a0.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/data_migrations/2_11_0.py +0 -168
  57. fractal_server/tasks/v1/_TaskCollectPip.py +0 -103
  58. fractal_server/tasks/v1/__init__.py +0 -0
  59. fractal_server/tasks/v1/background_operations.py +0 -352
  60. fractal_server/tasks/v1/endpoint_operations.py +0 -156
  61. fractal_server/tasks/v1/get_collection_data.py +0 -14
  62. fractal_server/tasks/v1/utils.py +0 -67
  63. {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0a0.dist-info}/LICENSE +0 -0
  64. {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0a0.dist-info}/WHEEL +0 -0
  65. {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0a0.dist-info}/entry_points.txt +0 -0
@@ -1,478 +0,0 @@
1
- """
2
- Auxiliary functions to get object from the database or perform simple checks
3
- """
4
- from typing import Any
5
- from typing import Literal
6
- from typing import Optional
7
- from typing import Union
8
-
9
- from fastapi import HTTPException
10
- from fastapi import status
11
- from sqlmodel import select
12
- from sqlmodel.sql.expression import SelectOfScalar
13
-
14
- from .....config import get_settings
15
- from .....syringe import Inject
16
- from ....db import AsyncSession
17
- from ....models.v1 import ApplyWorkflow
18
- from ....models.v1 import Dataset
19
- from ....models.v1 import LinkUserProject
20
- from ....models.v1 import Project
21
- from ....models.v1 import Task
22
- from ....models.v1 import Workflow
23
- from ....models.v1 import WorkflowTask
24
- from ....schemas.v1 import JobStatusTypeV1
25
- from ...aux.validate_user_settings import verify_user_has_settings
26
- from fractal_server.app.models import UserOAuth
27
-
28
-
29
- def _raise_if_v1_is_read_only() -> None:
30
- settings = Inject(get_settings)
31
- if settings.FRACTAL_API_V1_MODE == "include_read_only":
32
- raise HTTPException(
33
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
34
- detail="Legacy API is in read-only mode.",
35
- )
36
-
37
-
38
- async def _get_project_check_owner(
39
- *,
40
- project_id: int,
41
- user_id: int,
42
- db: AsyncSession,
43
- ) -> Project:
44
- """
45
- Check that user is a member of project and return the project.
46
-
47
- Args:
48
- project_id:
49
- user_id:
50
- db:
51
-
52
- Returns:
53
- The project object
54
-
55
- Raises:
56
- HTTPException(status_code=403_FORBIDDEN):
57
- If the user is not a member of the project
58
- HTTPException(status_code=404_NOT_FOUND):
59
- If the project does not exist
60
- """
61
- project = await db.get(Project, project_id)
62
- link_user_project = await db.get(LinkUserProject, (project_id, user_id))
63
- if not project:
64
- raise HTTPException(
65
- status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
66
- )
67
- if not link_user_project:
68
- raise HTTPException(
69
- status_code=status.HTTP_403_FORBIDDEN,
70
- detail=f"Not allowed on project {project_id}",
71
- )
72
- return project
73
-
74
-
75
- async def _get_workflow_check_owner(
76
- *,
77
- workflow_id: int,
78
- project_id: int,
79
- user_id: int,
80
- db: AsyncSession,
81
- ) -> Workflow:
82
- """
83
- Get a workflow and a project, after access control on the project.
84
-
85
- Args:
86
- workflow_id:
87
- project_id:
88
- user_id:
89
- db:
90
-
91
- Returns:
92
- The workflow object.
93
-
94
- Raises:
95
- HTTPException(status_code=404_NOT_FOUND):
96
- If the workflow does not exist
97
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
98
- If the workflow is not associated to the project
99
- """
100
-
101
- # Access control for project
102
- project = await _get_project_check_owner(
103
- project_id=project_id, user_id=user_id, db=db
104
- )
105
- # Get workflow
106
- workflow = await db.get(Workflow, workflow_id)
107
- if not workflow:
108
- raise HTTPException(
109
- status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found"
110
- )
111
- if workflow.project_id != project.id:
112
- raise HTTPException(
113
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
114
- detail=(f"Invalid {project_id=} for {workflow_id=}."),
115
- )
116
-
117
- # Refresh so that workflow.project relationship is loaded (see discussion
118
- # in issue #1063)
119
- await db.refresh(workflow)
120
-
121
- return workflow
122
-
123
-
124
- async def _get_workflow_task_check_owner(
125
- *,
126
- project_id: int,
127
- workflow_id: int,
128
- workflow_task_id: int,
129
- user_id: int,
130
- db: AsyncSession,
131
- ) -> tuple[WorkflowTask, Workflow]:
132
- """
133
- Check that user has access to Workflow and WorkflowTask.
134
-
135
- Args:
136
- project_id:
137
- workflow_id:
138
- workflow_task_id:
139
- user_id:
140
- db:
141
-
142
- Returns:
143
- Tuple of WorkflowTask and Workflow objects.
144
-
145
- Raises:
146
- HTTPException(status_code=404_NOT_FOUND):
147
- If the WorkflowTask does not exist
148
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
149
- If the WorkflowTask is not associated to the Workflow
150
- """
151
-
152
- # Access control for workflow
153
- workflow = await _get_workflow_check_owner(
154
- workflow_id=workflow_id, project_id=project_id, user_id=user_id, db=db
155
- )
156
-
157
- # If WorkflowTask is not in the db, exit
158
- workflow_task = await db.get(WorkflowTask, workflow_task_id)
159
- if not workflow_task:
160
- raise HTTPException(
161
- status_code=status.HTTP_404_NOT_FOUND,
162
- detail="WorkflowTask not found",
163
- )
164
-
165
- # If WorkflowTask is not part of the expected Workflow, exit
166
- if workflow_id != workflow_task.workflow_id:
167
- raise HTTPException(
168
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
169
- detail=f"Invalid {workflow_id=} for {workflow_task_id=}",
170
- )
171
-
172
- return workflow_task, workflow
173
-
174
-
175
- async def _check_workflow_exists(
176
- *,
177
- name: str,
178
- project_id: int,
179
- db: AsyncSession,
180
- ) -> None:
181
- """
182
- Check that no other workflow of this project has the same name.
183
-
184
- Args:
185
- name: Workflow name
186
- project_id: Project ID
187
- db:
188
-
189
- Raises:
190
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
191
- If such a workflow already exists
192
- """
193
- stm = (
194
- select(Workflow)
195
- .where(Workflow.name == name)
196
- .where(Workflow.project_id == project_id)
197
- )
198
- res = await db.execute(stm)
199
- if res.scalars().all():
200
- raise HTTPException(
201
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
202
- detail=f"Workflow with {name=} and {project_id=} already exists.",
203
- )
204
-
205
-
206
- async def _check_project_exists(
207
- *,
208
- project_name: str,
209
- user_id: int,
210
- db: AsyncSession,
211
- ) -> None:
212
- """
213
- Check that no other project with this name exists for this user.
214
-
215
- Args:
216
- project_name: Project name
217
- user_id: User ID
218
- db:
219
-
220
- Raises:
221
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
222
- If such a project already exists
223
- """
224
- stm = (
225
- select(Project)
226
- .join(LinkUserProject)
227
- .where(Project.name == project_name)
228
- .where(LinkUserProject.user_id == user_id)
229
- )
230
- res = await db.execute(stm)
231
- if res.scalars().all():
232
- raise HTTPException(
233
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
234
- detail=f"Project name ({project_name}) already in use",
235
- )
236
-
237
-
238
- async def _get_dataset_check_owner(
239
- *,
240
- project_id: int,
241
- dataset_id: int,
242
- user_id: int,
243
- db: AsyncSession,
244
- ) -> dict[Literal["dataset", "project"], Union[Dataset, Project]]:
245
- """
246
- Get a dataset and a project, after access control on the project
247
-
248
- Args:
249
- project_id:
250
- dataset_id:
251
- user_id:
252
- db:
253
-
254
- Returns:
255
- Dictionary with the dataset and project objects (keys: `dataset`,
256
- `project`).
257
-
258
- Raises:
259
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
260
- If the dataset is not associated to the project
261
- """
262
-
263
- # Access control for project
264
- project = await _get_project_check_owner(
265
- project_id=project_id, user_id=user_id, db=db
266
- )
267
- # Get dataset
268
- dataset = await db.get(Dataset, dataset_id)
269
- if not dataset:
270
- raise HTTPException(
271
- status_code=status.HTTP_404_NOT_FOUND, detail="Dataset not found"
272
- )
273
- if dataset.project_id != project_id:
274
- raise HTTPException(
275
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
276
- detail=f"Invalid {project_id=} for {dataset_id=}",
277
- )
278
-
279
- # Refresh so that dataset.project relationship is loaded (see discussion
280
- # in issue #1063)
281
- await db.refresh(dataset)
282
-
283
- return dict(dataset=dataset, project=project)
284
-
285
-
286
- async def _get_job_check_owner(
287
- *,
288
- project_id: int,
289
- job_id: int,
290
- user_id: int,
291
- db: AsyncSession,
292
- ) -> dict[Literal["job", "project"], Union[ApplyWorkflow, Project]]:
293
- """
294
- Get a job and a project, after access control on the project
295
-
296
- Args:
297
- project_id:
298
- job_id:
299
- user_id:
300
- db:
301
-
302
- Returns:
303
- Dictionary with the job and project objects (keys: `job`,
304
- `project`).
305
-
306
- Raises:
307
- HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
308
- If the job is not associated to the project
309
- """
310
- # Access control for project
311
- project = await _get_project_check_owner(
312
- project_id=project_id, user_id=user_id, db=db
313
- )
314
- # Get dataset
315
- job = await db.get(ApplyWorkflow, job_id)
316
- if not job:
317
- raise HTTPException(
318
- status_code=status.HTTP_404_NOT_FOUND, detail="Job not found"
319
- )
320
- if job.project_id != project_id:
321
- raise HTTPException(
322
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
323
- detail=f"Invalid {project_id=} for {job_id=}",
324
- )
325
- return dict(job=job, project=project)
326
-
327
-
328
- async def _get_task_check_owner(
329
- *,
330
- task_id: int,
331
- user: UserOAuth,
332
- db: AsyncSession,
333
- ) -> Task:
334
- """
335
- Get a task, after access control.
336
-
337
- This check constitutes a preliminary version of access control:
338
- if the current user is not a superuser and differs from the task owner
339
- (including when `owner is None`), we raise an 403 HTTP Exception.
340
-
341
- Args:
342
- task_id:
343
- user:
344
- db:
345
-
346
- Returns:
347
- The task object.
348
-
349
- Raises:
350
- HTTPException(status_code=404_NOT_FOUND):
351
- If the task does not exist
352
- HTTPException(status_code=403_FORBIDDEN):
353
- If the user does not have rights to edit this task.
354
- """
355
- task = await db.get(Task, task_id)
356
- if not task:
357
- raise HTTPException(
358
- status_code=status.HTTP_404_NOT_FOUND,
359
- detail=f"Task {task_id} not found.",
360
- )
361
-
362
- if not user.is_superuser:
363
- if task.owner is None:
364
- raise HTTPException(
365
- status_code=status.HTTP_403_FORBIDDEN,
366
- detail=(
367
- "Only a superuser can modify a Task with `owner=None`."
368
- ),
369
- )
370
- else:
371
- if user.username:
372
- owner = user.username
373
- else:
374
- verify_user_has_settings(user)
375
- owner = user.settings.slurm_user
376
- if owner != task.owner:
377
- raise HTTPException(
378
- status_code=status.HTTP_403_FORBIDDEN,
379
- detail=(
380
- f"Current user ({owner}) cannot modify Task {task.id} "
381
- f"with different owner ({task.owner})."
382
- ),
383
- )
384
- return task
385
-
386
-
387
- def _get_submitted_jobs_statement() -> SelectOfScalar:
388
- """
389
- Returns:
390
- A sqlmodel statement that selects all `ApplyWorkflow`s with
391
- `ApplyWorkflow.status` equal to `submitted`.
392
- """
393
- stm = select(ApplyWorkflow).where(
394
- ApplyWorkflow.status == JobStatusTypeV1.SUBMITTED
395
- )
396
- return stm
397
-
398
-
399
- async def _workflow_insert_task(
400
- *,
401
- workflow_id: int,
402
- task_id: int,
403
- args: Optional[dict[str, Any]] = None,
404
- meta: Optional[dict[str, Any]] = None,
405
- order: Optional[int] = None,
406
- db: AsyncSession,
407
- ) -> WorkflowTask:
408
- """
409
- Insert a new WorkflowTask into Workflow.task_list
410
-
411
- Args:
412
- task_id: TBD
413
- args: TBD
414
- meta: TBD
415
- order: TBD
416
- db: TBD
417
- """
418
- db_workflow = await db.get(Workflow, workflow_id)
419
- if db_workflow is None:
420
- raise ValueError(f"Workflow {workflow_id} does not exist")
421
-
422
- if order is None:
423
- order = len(db_workflow.task_list)
424
-
425
- # Get task from db, and extract default arguments via a Task property
426
- # method
427
- db_task = await db.get(Task, task_id)
428
- if db_task is None:
429
- raise ValueError(f"Task {task_id} does not exist")
430
-
431
- default_args = db_task.default_args_from_args_schema
432
- # Override default_args with args
433
- actual_args = default_args.copy()
434
- if args is not None:
435
- for k, v in args.items():
436
- actual_args[k] = v
437
- if not actual_args:
438
- actual_args = None
439
-
440
- # Combine meta (higher priority) and db_task.meta (lower priority)
441
- wt_meta = (db_task.meta or {}).copy()
442
- wt_meta.update(meta or {})
443
- if not wt_meta:
444
- wt_meta = None
445
-
446
- # Create DB entry
447
- wf_task = WorkflowTask(task_id=task_id, args=actual_args, meta=wt_meta)
448
- db.add(wf_task)
449
- db_workflow.task_list.insert(order, wf_task)
450
- db_workflow.task_list.reorder() # type: ignore
451
- await db.commit()
452
- await db.refresh(wf_task)
453
-
454
- return wf_task
455
-
456
-
457
- async def clean_app_job_list_v1(
458
- db: AsyncSession, jobs_list: list[int]
459
- ) -> list[int]:
460
- """
461
- Remove from a job list all jobs with status different from submitted.
462
-
463
- Args:
464
- db: Async database session
465
- jobs_list: List of job IDs currently associated to the app.
466
-
467
- Return:
468
- List of IDs for submitted jobs.
469
- """
470
- stmt = select(ApplyWorkflow).where(ApplyWorkflow.id.in_(jobs_list))
471
- result = await db.execute(stmt)
472
- db_jobs_list = result.scalars().all()
473
- submitted_job_ids = [
474
- job.id
475
- for job in db_jobs_list
476
- if job.status == JobStatusTypeV1.SUBMITTED
477
- ]
478
- return submitted_job_ids