fractal-server 2.16.5__py3-none-any.whl → 2.17.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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +178 -52
- fractal_server/app/db/__init__.py +9 -11
- fractal_server/app/models/security.py +30 -22
- fractal_server/app/models/user_settings.py +5 -4
- fractal_server/app/models/v2/__init__.py +4 -0
- fractal_server/app/models/v2/job.py +3 -4
- fractal_server/app/models/v2/profile.py +16 -0
- fractal_server/app/models/v2/project.py +5 -0
- fractal_server/app/models/v2/resource.py +130 -0
- fractal_server/app/models/v2/task_group.py +4 -0
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/impersonate.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +51 -15
- fractal_server/app/routes/admin/v2/profile.py +100 -0
- fractal_server/app/routes/admin/v2/project.py +2 -2
- fractal_server/app/routes/admin/v2/resource.py +222 -0
- fractal_server/app/routes/admin/v2/task.py +59 -32
- fractal_server/app/routes/admin/v2/task_group.py +17 -12
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
- fractal_server/app/routes/api/__init__.py +45 -8
- fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
- fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
- fractal_server/app/routes/api/v2/dataset.py +10 -19
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +5 -5
- fractal_server/app/routes/api/v2/job.py +8 -8
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +15 -7
- fractal_server/app/routes/api/v2/status_legacy.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +49 -42
- fractal_server/app/routes/api/v2/task.py +26 -8
- fractal_server/app/routes/api/v2/task_collection.py +39 -50
- fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
- fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
- fractal_server/app/routes/api/v2/task_group.py +19 -9
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
- fractal_server/app/routes/api/v2/task_version_update.py +3 -3
- fractal_server/app/routes/api/v2/workflow.py +9 -9
- fractal_server/app/routes/api/v2/workflow_import.py +29 -16
- fractal_server/app/routes/api/v2/workflowtask.py +5 -5
- fractal_server/app/routes/auth/__init__.py +34 -5
- fractal_server/app/routes/auth/_aux_auth.py +39 -20
- fractal_server/app/routes/auth/current_user.py +56 -67
- fractal_server/app/routes/auth/group.py +29 -46
- fractal_server/app/routes/auth/oauth.py +55 -38
- fractal_server/app/routes/auth/register.py +2 -2
- fractal_server/app/routes/auth/router.py +4 -2
- fractal_server/app/routes/auth/users.py +29 -53
- fractal_server/app/routes/aux/_runner.py +2 -1
- fractal_server/app/routes/aux/validate_user_profile.py +62 -0
- fractal_server/app/schemas/__init__.py +0 -1
- fractal_server/app/schemas/user.py +43 -13
- fractal_server/app/schemas/user_group.py +2 -1
- fractal_server/app/schemas/v2/__init__.py +12 -0
- fractal_server/app/schemas/v2/profile.py +78 -0
- fractal_server/app/schemas/v2/resource.py +137 -0
- fractal_server/app/schemas/v2/task_collection.py +11 -3
- fractal_server/app/schemas/v2/task_group.py +5 -0
- fractal_server/app/security/__init__.py +174 -75
- fractal_server/app/security/signup_email.py +52 -34
- fractal_server/config/__init__.py +27 -0
- fractal_server/config/_data.py +68 -0
- fractal_server/config/_database.py +59 -0
- fractal_server/config/_email.py +133 -0
- fractal_server/config/_main.py +78 -0
- fractal_server/config/_oauth.py +69 -0
- fractal_server/config/_settings_config.py +7 -0
- fractal_server/data_migrations/2_17_0.py +339 -0
- fractal_server/images/tools.py +3 -3
- fractal_server/logger.py +3 -3
- fractal_server/main.py +17 -23
- fractal_server/migrations/naming_convention.py +1 -1
- fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
- fractal_server/runner/config/__init__.py +2 -0
- fractal_server/runner/config/_local.py +21 -0
- fractal_server/runner/config/_slurm.py +129 -0
- fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
- fractal_server/runner/exceptions.py +4 -0
- fractal_server/runner/executors/base_runner.py +17 -7
- fractal_server/runner/executors/local/get_local_config.py +21 -86
- fractal_server/runner/executors/local/runner.py +48 -5
- fractal_server/runner/executors/slurm_common/_batching.py +2 -2
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
- fractal_server/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
- fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
- fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
- fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
- fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
- fractal_server/runner/v2/_local.py +36 -21
- fractal_server/runner/v2/_slurm_ssh.py +41 -4
- fractal_server/runner/v2/_slurm_sudo.py +42 -12
- fractal_server/runner/v2/db_tools.py +1 -1
- fractal_server/runner/v2/runner.py +3 -11
- fractal_server/runner/v2/runner_functions.py +42 -28
- fractal_server/runner/v2/submit_workflow.py +88 -109
- fractal_server/runner/versions.py +8 -3
- fractal_server/ssh/_fabric.py +6 -6
- fractal_server/tasks/config/__init__.py +3 -0
- fractal_server/tasks/config/_pixi.py +127 -0
- fractal_server/tasks/config/_python.py +51 -0
- fractal_server/tasks/v2/local/_utils.py +7 -7
- fractal_server/tasks/v2/local/collect.py +13 -5
- fractal_server/tasks/v2/local/collect_pixi.py +26 -10
- fractal_server/tasks/v2/local/deactivate.py +7 -1
- fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
- fractal_server/tasks/v2/local/delete.py +5 -1
- fractal_server/tasks/v2/local/reactivate.py +13 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
- fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
- fractal_server/tasks/v2/ssh/_utils.py +6 -7
- fractal_server/tasks/v2/ssh/collect.py +19 -12
- fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
- fractal_server/tasks/v2/ssh/deactivate.py +12 -8
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
- fractal_server/tasks/v2/ssh/delete.py +12 -9
- fractal_server/tasks/v2/ssh/reactivate.py +18 -12
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
- fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
- fractal_server/tasks/v2/utils_database.py +2 -2
- fractal_server/tasks/v2/utils_pixi.py +3 -0
- fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
- fractal_server/tasks/v2/utils_templates.py +7 -10
- fractal_server/utils.py +1 -1
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/METADATA +8 -10
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/RECORD +137 -118
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +1 -1
- fractal_server/app/routes/aux/validate_user_settings.py +0 -73
- fractal_server/app/schemas/user_settings.py +0 -67
- fractal_server/app/user_settings.py +0 -42
- fractal_server/config.py +0 -906
- fractal_server/data_migrations/2_14_10.py +0 -48
- fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
- /fractal_server/{runner → app}/shutdown.py +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -6,7 +6,9 @@ from fastapi import APIRouter
|
|
|
6
6
|
from .accounting import router as accounting_router
|
|
7
7
|
from .impersonate import router as impersonate_router
|
|
8
8
|
from .job import router as job_router
|
|
9
|
+
from .profile import router as profile_router
|
|
9
10
|
from .project import router as project_router
|
|
11
|
+
from .resource import router as resource_router
|
|
10
12
|
from .task import router as task_router
|
|
11
13
|
from .task_group import router as task_group_router
|
|
12
14
|
from .task_group_lifecycle import router as task_group_lifecycle_router
|
|
@@ -22,3 +24,5 @@ router_admin_v2.include_router(
|
|
|
22
24
|
task_group_lifecycle_router, prefix="/task-group"
|
|
23
25
|
)
|
|
24
26
|
router_admin_v2.include_router(impersonate_router, prefix="/impersonate")
|
|
27
|
+
router_admin_v2.include_router(resource_router, prefix="/resource")
|
|
28
|
+
router_admin_v2.include_router(profile_router, prefix="/profile")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from sqlmodel import select
|
|
4
|
+
|
|
5
|
+
from fractal_server.app.db import AsyncSession
|
|
6
|
+
from fractal_server.app.models.v2 import Profile
|
|
7
|
+
from fractal_server.app.models.v2 import Resource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def _get_resource_or_404(
|
|
11
|
+
*,
|
|
12
|
+
resource_id: int,
|
|
13
|
+
db: AsyncSession,
|
|
14
|
+
) -> Resource:
|
|
15
|
+
resource = await db.get(Resource, resource_id)
|
|
16
|
+
if resource is None:
|
|
17
|
+
raise HTTPException(
|
|
18
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
19
|
+
detail=f"Resource {resource_id} not found",
|
|
20
|
+
)
|
|
21
|
+
return resource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _get_profile_or_404(
|
|
25
|
+
*,
|
|
26
|
+
profile_id: int,
|
|
27
|
+
db: AsyncSession,
|
|
28
|
+
) -> Profile:
|
|
29
|
+
profile = await db.get(Profile, profile_id)
|
|
30
|
+
if profile is None:
|
|
31
|
+
raise HTTPException(
|
|
32
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
33
|
+
detail=f"Profile {profile_id} not found",
|
|
34
|
+
)
|
|
35
|
+
return profile
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _check_profile_name(*, name: str, db: AsyncSession) -> None:
|
|
39
|
+
res = await db.execute(select(Profile).where(Profile.name == name))
|
|
40
|
+
namesake = res.scalars().one_or_none()
|
|
41
|
+
if namesake is not None:
|
|
42
|
+
raise HTTPException(
|
|
43
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
44
|
+
detail=f"Profile with name '{name}' already exists.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _check_resource_name(*, name: str, db: AsyncSession) -> None:
|
|
49
|
+
res = await db.execute(select(Resource).where(Resource.name == name))
|
|
50
|
+
namesake = res.scalars().one_or_none()
|
|
51
|
+
if namesake is not None:
|
|
52
|
+
raise HTTPException(
|
|
53
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
54
|
+
detail=f"Resource with name '{name}' already exists.",
|
|
55
|
+
)
|
|
@@ -13,7 +13,7 @@ from fractal_server.app.db import get_async_db
|
|
|
13
13
|
from fractal_server.app.models import UserOAuth
|
|
14
14
|
from fractal_server.app.models.v2 import AccountingRecord
|
|
15
15
|
from fractal_server.app.models.v2 import AccountingRecordSlurm
|
|
16
|
-
from fractal_server.app.routes.auth import
|
|
16
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
17
17
|
from fractal_server.app.routes.pagination import get_pagination_params
|
|
18
18
|
from fractal_server.app.routes.pagination import PaginationRequest
|
|
19
19
|
from fractal_server.app.routes.pagination import PaginationResponse
|
|
@@ -34,7 +34,7 @@ async def query_accounting(
|
|
|
34
34
|
query: AccountingQuery,
|
|
35
35
|
# Dependencies
|
|
36
36
|
pagination: PaginationRequest = Depends(get_pagination_params),
|
|
37
|
-
superuser: UserOAuth = Depends(
|
|
37
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
38
38
|
db: AsyncSession = Depends(get_async_db),
|
|
39
39
|
) -> PaginationResponse[AccountingRecordRead]:
|
|
40
40
|
page = pagination.page
|
|
@@ -79,7 +79,7 @@ async def query_accounting(
|
|
|
79
79
|
async def query_accounting_slurm(
|
|
80
80
|
query: AccountingQuery,
|
|
81
81
|
# dependencies
|
|
82
|
-
superuser: UserOAuth = Depends(
|
|
82
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
83
83
|
db: AsyncSession = Depends(get_async_db),
|
|
84
84
|
) -> JSONResponse:
|
|
85
85
|
stm = select(AccountingRecordSlurm.slurm_job_ids)
|
|
@@ -6,7 +6,7 @@ from fastapi_users.authentication import JWTStrategy
|
|
|
6
6
|
from fractal_server.app.db import AsyncSession
|
|
7
7
|
from fractal_server.app.db import get_async_db
|
|
8
8
|
from fractal_server.app.models import UserOAuth
|
|
9
|
-
from fractal_server.app.routes.auth import
|
|
9
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
10
10
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
|
11
11
|
from fractal_server.config import get_settings
|
|
12
12
|
from fractal_server.syringe import Inject
|
|
@@ -17,7 +17,7 @@ router = APIRouter()
|
|
|
17
17
|
@router.get("/{user_id}/")
|
|
18
18
|
async def impersonate_user(
|
|
19
19
|
user_id: int,
|
|
20
|
-
superuser: UserOAuth = Depends(
|
|
20
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
21
21
|
db: AsyncSession = Depends(get_async_db),
|
|
22
22
|
) -> JSONResponse:
|
|
23
23
|
user = await _user_or_404(user_id, db)
|
|
@@ -7,6 +7,7 @@ from fastapi import Response
|
|
|
7
7
|
from fastapi import status
|
|
8
8
|
from fastapi.responses import StreamingResponse
|
|
9
9
|
from pydantic.types import AwareDatetime
|
|
10
|
+
from sqlalchemy import func
|
|
10
11
|
from sqlmodel import select
|
|
11
12
|
|
|
12
13
|
from fractal_server.app.db import AsyncSession
|
|
@@ -16,9 +17,12 @@ from fractal_server.app.models.v2 import HistoryRun
|
|
|
16
17
|
from fractal_server.app.models.v2 import HistoryUnit
|
|
17
18
|
from fractal_server.app.models.v2 import JobV2
|
|
18
19
|
from fractal_server.app.models.v2 import ProjectV2
|
|
19
|
-
from fractal_server.app.routes.auth import
|
|
20
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
20
21
|
from fractal_server.app.routes.aux._job import _write_shutdown_file
|
|
21
22
|
from fractal_server.app.routes.aux._runner import _check_shutdown_is_supported
|
|
23
|
+
from fractal_server.app.routes.pagination import get_pagination_params
|
|
24
|
+
from fractal_server.app.routes.pagination import PaginationRequest
|
|
25
|
+
from fractal_server.app.routes.pagination import PaginationResponse
|
|
22
26
|
from fractal_server.app.schemas.v2 import HistoryUnitStatus
|
|
23
27
|
from fractal_server.app.schemas.v2 import JobReadV2
|
|
24
28
|
from fractal_server.app.schemas.v2 import JobStatusTypeV2
|
|
@@ -30,7 +34,7 @@ from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
|
|
|
30
34
|
router = APIRouter()
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
@router.get("/", response_model=
|
|
37
|
+
@router.get("/", response_model=PaginationResponse[JobReadV2])
|
|
34
38
|
async def view_job(
|
|
35
39
|
id: int | None = None,
|
|
36
40
|
user_id: int | None = None,
|
|
@@ -43,9 +47,10 @@ async def view_job(
|
|
|
43
47
|
end_timestamp_min: AwareDatetime | None = None,
|
|
44
48
|
end_timestamp_max: AwareDatetime | None = None,
|
|
45
49
|
log: bool = True,
|
|
46
|
-
|
|
50
|
+
pagination: PaginationRequest = Depends(get_pagination_params),
|
|
51
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
47
52
|
db: AsyncSession = Depends(get_async_db),
|
|
48
|
-
) ->
|
|
53
|
+
) -> PaginationResponse[JobReadV2]:
|
|
49
54
|
"""
|
|
50
55
|
Query `ApplyWorkflow` table.
|
|
51
56
|
|
|
@@ -68,50 +73,81 @@ async def view_job(
|
|
|
68
73
|
`job.log` is set to `None`.
|
|
69
74
|
"""
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
# Assign pagination parameters
|
|
77
|
+
page = pagination.page
|
|
78
|
+
page_size = pagination.page_size
|
|
72
79
|
|
|
80
|
+
# Prepare statements
|
|
81
|
+
stm = select(JobV2).order_by(JobV2.start_timestamp.desc())
|
|
82
|
+
stm_count = select(func.count(JobV2.id))
|
|
73
83
|
if id is not None:
|
|
74
84
|
stm = stm.where(JobV2.id == id)
|
|
85
|
+
stm_count = stm_count.where(JobV2.id == id)
|
|
75
86
|
if user_id is not None:
|
|
76
87
|
stm = stm.join(ProjectV2).where(
|
|
77
88
|
ProjectV2.user_list.any(UserOAuth.id == user_id)
|
|
78
89
|
)
|
|
90
|
+
stm_count = stm_count.join(ProjectV2).where(
|
|
91
|
+
ProjectV2.user_list.any(UserOAuth.id == user_id)
|
|
92
|
+
)
|
|
79
93
|
if project_id is not None:
|
|
80
94
|
stm = stm.where(JobV2.project_id == project_id)
|
|
95
|
+
stm_count = stm_count.where(JobV2.project_id == project_id)
|
|
81
96
|
if dataset_id is not None:
|
|
82
97
|
stm = stm.where(JobV2.dataset_id == dataset_id)
|
|
98
|
+
stm_count = stm_count.where(JobV2.dataset_id == dataset_id)
|
|
83
99
|
if workflow_id is not None:
|
|
84
100
|
stm = stm.where(JobV2.workflow_id == workflow_id)
|
|
101
|
+
stm_count = stm_count.where(JobV2.workflow_id == workflow_id)
|
|
85
102
|
if status is not None:
|
|
86
103
|
stm = stm.where(JobV2.status == status)
|
|
104
|
+
stm_count = stm_count.where(JobV2.status == status)
|
|
87
105
|
if start_timestamp_min is not None:
|
|
88
|
-
start_timestamp_min = start_timestamp_min
|
|
89
106
|
stm = stm.where(JobV2.start_timestamp >= start_timestamp_min)
|
|
107
|
+
stm_count = stm_count.where(
|
|
108
|
+
JobV2.start_timestamp >= start_timestamp_min
|
|
109
|
+
)
|
|
90
110
|
if start_timestamp_max is not None:
|
|
91
|
-
start_timestamp_max = start_timestamp_max
|
|
92
111
|
stm = stm.where(JobV2.start_timestamp <= start_timestamp_max)
|
|
112
|
+
stm_count = stm_count.where(
|
|
113
|
+
JobV2.start_timestamp <= start_timestamp_max
|
|
114
|
+
)
|
|
93
115
|
if end_timestamp_min is not None:
|
|
94
|
-
end_timestamp_min = end_timestamp_min
|
|
95
116
|
stm = stm.where(JobV2.end_timestamp >= end_timestamp_min)
|
|
117
|
+
stm_count = stm_count.where(JobV2.end_timestamp >= end_timestamp_min)
|
|
96
118
|
if end_timestamp_max is not None:
|
|
97
|
-
end_timestamp_max = end_timestamp_max
|
|
98
119
|
stm = stm.where(JobV2.end_timestamp <= end_timestamp_max)
|
|
120
|
+
stm_count = stm_count.where(JobV2.end_timestamp <= end_timestamp_max)
|
|
99
121
|
|
|
122
|
+
# Find total number of elements
|
|
123
|
+
res_total_count = await db.execute(stm_count)
|
|
124
|
+
total_count = res_total_count.scalar()
|
|
125
|
+
if page_size is None:
|
|
126
|
+
page_size = total_count
|
|
127
|
+
else:
|
|
128
|
+
stm = stm.offset((page - 1) * page_size).limit(page_size)
|
|
129
|
+
|
|
130
|
+
# Get `page_size` rows
|
|
100
131
|
res = await db.execute(stm)
|
|
101
132
|
job_list = res.scalars().all()
|
|
102
|
-
|
|
133
|
+
|
|
103
134
|
if not log:
|
|
104
135
|
for job in job_list:
|
|
105
136
|
setattr(job, "log", None)
|
|
106
137
|
|
|
107
|
-
return
|
|
138
|
+
return PaginationResponse[JobReadV2](
|
|
139
|
+
total_count=total_count,
|
|
140
|
+
page_size=page_size,
|
|
141
|
+
current_page=page,
|
|
142
|
+
items=[job.model_dump() for job in job_list],
|
|
143
|
+
)
|
|
108
144
|
|
|
109
145
|
|
|
110
146
|
@router.get("/{job_id}/", response_model=JobReadV2)
|
|
111
147
|
async def view_single_job(
|
|
112
148
|
job_id: int,
|
|
113
149
|
show_tmp_logs: bool = False,
|
|
114
|
-
user: UserOAuth = Depends(
|
|
150
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
115
151
|
db: AsyncSession = Depends(get_async_db),
|
|
116
152
|
) -> JobReadV2:
|
|
117
153
|
job = await db.get(JobV2, job_id)
|
|
@@ -136,7 +172,7 @@ async def view_single_job(
|
|
|
136
172
|
async def update_job(
|
|
137
173
|
job_update: JobUpdateV2,
|
|
138
174
|
job_id: int,
|
|
139
|
-
user: UserOAuth = Depends(
|
|
175
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
140
176
|
db: AsyncSession = Depends(get_async_db),
|
|
141
177
|
) -> JobReadV2 | None:
|
|
142
178
|
"""
|
|
@@ -200,7 +236,7 @@ async def update_job(
|
|
|
200
236
|
@router.get("/{job_id}/stop/", status_code=202)
|
|
201
237
|
async def stop_job(
|
|
202
238
|
job_id: int,
|
|
203
|
-
user: UserOAuth = Depends(
|
|
239
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
204
240
|
db: AsyncSession = Depends(get_async_db),
|
|
205
241
|
) -> Response:
|
|
206
242
|
"""
|
|
@@ -224,7 +260,7 @@ async def stop_job(
|
|
|
224
260
|
@router.get("/{job_id}/download/", response_class=StreamingResponse)
|
|
225
261
|
async def download_job_logs(
|
|
226
262
|
job_id: int,
|
|
227
|
-
user: UserOAuth = Depends(
|
|
263
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
228
264
|
db: AsyncSession = Depends(get_async_db),
|
|
229
265
|
) -> StreamingResponse:
|
|
230
266
|
"""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
from fastapi import Response
|
|
5
|
+
from fastapi import status
|
|
6
|
+
from sqlmodel import func
|
|
7
|
+
from sqlmodel import select
|
|
8
|
+
|
|
9
|
+
from ._aux_functions import _check_profile_name
|
|
10
|
+
from ._aux_functions import _get_profile_or_404
|
|
11
|
+
from fractal_server.app.db import AsyncSession
|
|
12
|
+
from fractal_server.app.db import get_async_db
|
|
13
|
+
from fractal_server.app.models import Profile
|
|
14
|
+
from fractal_server.app.models import UserOAuth
|
|
15
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
16
|
+
from fractal_server.app.schemas.v2 import ProfileCreate
|
|
17
|
+
from fractal_server.app.schemas.v2 import ProfileRead
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.get("/{profile_id}/", response_model=ProfileRead, status_code=200)
|
|
23
|
+
async def get_single_profile(
|
|
24
|
+
profile_id: int,
|
|
25
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
26
|
+
db: AsyncSession = Depends(get_async_db),
|
|
27
|
+
) -> ProfileRead:
|
|
28
|
+
"""
|
|
29
|
+
Query single `Profile`.
|
|
30
|
+
"""
|
|
31
|
+
profile = await _get_profile_or_404(profile_id=profile_id, db=db)
|
|
32
|
+
return profile
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/", response_model=list[ProfileRead], status_code=200)
|
|
36
|
+
async def get_profile_list(
|
|
37
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
38
|
+
db: AsyncSession = Depends(get_async_db),
|
|
39
|
+
) -> ProfileRead:
|
|
40
|
+
"""
|
|
41
|
+
Query single `Profile`.
|
|
42
|
+
"""
|
|
43
|
+
res = await db.execute(select(Profile).order_by(Profile.id))
|
|
44
|
+
profiles = res.scalars().all()
|
|
45
|
+
return profiles
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.put("/{profile_id}/", response_model=ProfileRead, status_code=200)
|
|
49
|
+
async def put_profile(
|
|
50
|
+
profile_id: int,
|
|
51
|
+
profile_update: ProfileCreate,
|
|
52
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
53
|
+
db: AsyncSession = Depends(get_async_db),
|
|
54
|
+
) -> ProfileRead:
|
|
55
|
+
"""
|
|
56
|
+
Override single `Profile`.
|
|
57
|
+
"""
|
|
58
|
+
profile = await _get_profile_or_404(profile_id=profile_id, db=db)
|
|
59
|
+
|
|
60
|
+
if profile_update.name and profile_update.name != profile.name:
|
|
61
|
+
await _check_profile_name(name=profile_update.name, db=db)
|
|
62
|
+
|
|
63
|
+
for key, value in profile_update.model_dump().items():
|
|
64
|
+
setattr(profile, key, value)
|
|
65
|
+
await db.commit()
|
|
66
|
+
await db.refresh(profile)
|
|
67
|
+
return profile
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.delete("/{profile_id}/", status_code=204)
|
|
71
|
+
async def delete_profile(
|
|
72
|
+
profile_id: int,
|
|
73
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
74
|
+
db: AsyncSession = Depends(get_async_db),
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Delete single `Profile`.
|
|
78
|
+
"""
|
|
79
|
+
profile = await _get_profile_or_404(profile_id=profile_id, db=db)
|
|
80
|
+
|
|
81
|
+
# Fail if at least one UserOAuth is associated with the Profile.
|
|
82
|
+
res = await db.execute(
|
|
83
|
+
select(func.count(UserOAuth.id)).where(
|
|
84
|
+
UserOAuth.profile_id == profile.id
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
associated_users_count = res.scalar()
|
|
88
|
+
if associated_users_count > 0:
|
|
89
|
+
raise HTTPException(
|
|
90
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
91
|
+
detail=(
|
|
92
|
+
f"Cannot delete Profile {profile_id} because it's associated"
|
|
93
|
+
f" with {associated_users_count} UserOAuths."
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Delete
|
|
98
|
+
await db.delete(profile)
|
|
99
|
+
await db.commit()
|
|
100
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
@@ -6,7 +6,7 @@ from fractal_server.app.db import AsyncSession
|
|
|
6
6
|
from fractal_server.app.db import get_async_db
|
|
7
7
|
from fractal_server.app.models import UserOAuth
|
|
8
8
|
from fractal_server.app.models.v2 import ProjectV2
|
|
9
|
-
from fractal_server.app.routes.auth import
|
|
9
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
10
10
|
from fractal_server.app.schemas.v2 import ProjectReadV2
|
|
11
11
|
|
|
12
12
|
router = APIRouter()
|
|
@@ -16,7 +16,7 @@ router = APIRouter()
|
|
|
16
16
|
async def view_project(
|
|
17
17
|
id: int | None = None,
|
|
18
18
|
user_id: int | None = None,
|
|
19
|
-
user: UserOAuth = Depends(
|
|
19
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
20
20
|
db: AsyncSession = Depends(get_async_db),
|
|
21
21
|
) -> list[ProjectReadV2]:
|
|
22
22
|
"""
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
from fastapi import Response
|
|
5
|
+
from fastapi import status
|
|
6
|
+
from sqlalchemy.exc import IntegrityError
|
|
7
|
+
from sqlmodel import select
|
|
8
|
+
|
|
9
|
+
from ._aux_functions import _check_resource_name
|
|
10
|
+
from ._aux_functions import _get_resource_or_404
|
|
11
|
+
from .profile import _check_profile_name
|
|
12
|
+
from fractal_server.app.db import AsyncSession
|
|
13
|
+
from fractal_server.app.db import get_async_db
|
|
14
|
+
from fractal_server.app.models import UserOAuth
|
|
15
|
+
from fractal_server.app.models.v2 import Profile
|
|
16
|
+
from fractal_server.app.models.v2 import Resource
|
|
17
|
+
from fractal_server.app.routes.auth import current_superuser_act
|
|
18
|
+
from fractal_server.app.schemas.v2 import ProfileCreate
|
|
19
|
+
from fractal_server.app.schemas.v2 import ProfileRead
|
|
20
|
+
from fractal_server.app.schemas.v2 import ResourceCreate
|
|
21
|
+
from fractal_server.app.schemas.v2 import ResourceRead
|
|
22
|
+
from fractal_server.config import get_settings
|
|
23
|
+
from fractal_server.syringe import Inject
|
|
24
|
+
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_resource_type_match_or_422(
|
|
29
|
+
resource: Resource,
|
|
30
|
+
new_profile: ProfileCreate,
|
|
31
|
+
) -> None:
|
|
32
|
+
if resource.type != new_profile.resource_type:
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
35
|
+
detail=(
|
|
36
|
+
f"{resource.type=} differs from {new_profile.resource_type=}."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_type_match_or_422(new_resource: ResourceCreate) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Handle case where `resource.type != FRACTAL_RUNNER_BACKEND`
|
|
44
|
+
"""
|
|
45
|
+
settings = Inject(get_settings)
|
|
46
|
+
if settings.FRACTAL_RUNNER_BACKEND != new_resource.type:
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
49
|
+
detail=(
|
|
50
|
+
f"{settings.FRACTAL_RUNNER_BACKEND=} != "
|
|
51
|
+
f"{new_resource.type=}"
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("/", response_model=list[ResourceRead], status_code=200)
|
|
57
|
+
async def get_resource_list(
|
|
58
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
59
|
+
db: AsyncSession = Depends(get_async_db),
|
|
60
|
+
) -> list[ResourceRead]:
|
|
61
|
+
"""
|
|
62
|
+
Query `Resource` table.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
stm = select(Resource).order_by(Resource.id)
|
|
66
|
+
res = await db.execute(stm)
|
|
67
|
+
resource_list = res.scalars().all()
|
|
68
|
+
|
|
69
|
+
return resource_list
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get("/{resource_id}/", response_model=ResourceRead, status_code=200)
|
|
73
|
+
async def get_resource(
|
|
74
|
+
resource_id: int,
|
|
75
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
76
|
+
db: AsyncSession = Depends(get_async_db),
|
|
77
|
+
) -> ResourceRead:
|
|
78
|
+
"""
|
|
79
|
+
Query single `Resource`.
|
|
80
|
+
"""
|
|
81
|
+
resource = await _get_resource_or_404(resource_id=resource_id, db=db)
|
|
82
|
+
|
|
83
|
+
return resource
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.post("/", response_model=ResourceRead, status_code=201)
|
|
87
|
+
async def post_resource(
|
|
88
|
+
resource_create: ResourceCreate,
|
|
89
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
90
|
+
db: AsyncSession = Depends(get_async_db),
|
|
91
|
+
) -> ResourceRead:
|
|
92
|
+
"""
|
|
93
|
+
Create new `Resource`.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Handle case where type!=FRACTAL_RUNNER_BACKEND
|
|
97
|
+
_check_type_match_or_422(resource_create)
|
|
98
|
+
|
|
99
|
+
await _check_resource_name(name=resource_create.name, db=db)
|
|
100
|
+
|
|
101
|
+
resource = Resource(**resource_create.model_dump())
|
|
102
|
+
db.add(resource)
|
|
103
|
+
await db.commit()
|
|
104
|
+
await db.refresh(resource)
|
|
105
|
+
|
|
106
|
+
return resource
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.put(
|
|
110
|
+
"/{resource_id}/",
|
|
111
|
+
response_model=ResourceRead,
|
|
112
|
+
status_code=200,
|
|
113
|
+
)
|
|
114
|
+
async def put_resource(
|
|
115
|
+
resource_id: int,
|
|
116
|
+
resource_update: ResourceCreate,
|
|
117
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
118
|
+
db: AsyncSession = Depends(get_async_db),
|
|
119
|
+
) -> ResourceRead:
|
|
120
|
+
"""
|
|
121
|
+
Overwrite a single `Resource`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Handle case where type!=FRACTAL_RUNNER_BACKEND
|
|
125
|
+
_check_type_match_or_422(resource_update)
|
|
126
|
+
|
|
127
|
+
resource = await _get_resource_or_404(resource_id=resource_id, db=db)
|
|
128
|
+
|
|
129
|
+
# Handle non-unique resource names
|
|
130
|
+
if resource_update.name and resource_update.name != resource.name:
|
|
131
|
+
await _check_resource_name(name=resource_update.name, db=db)
|
|
132
|
+
|
|
133
|
+
# Prepare new db object
|
|
134
|
+
for key, value in resource_update.model_dump().items():
|
|
135
|
+
setattr(resource, key, value)
|
|
136
|
+
|
|
137
|
+
await db.commit()
|
|
138
|
+
await db.refresh(resource)
|
|
139
|
+
return resource
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.delete("/{resource_id}/", status_code=204)
|
|
143
|
+
async def delete_resource(
|
|
144
|
+
resource_id: int,
|
|
145
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
146
|
+
db: AsyncSession = Depends(get_async_db),
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
Delete single `Resource`.
|
|
150
|
+
"""
|
|
151
|
+
resource = await _get_resource_or_404(resource_id=resource_id, db=db)
|
|
152
|
+
try:
|
|
153
|
+
await db.delete(resource)
|
|
154
|
+
await db.commit()
|
|
155
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
156
|
+
except IntegrityError as e:
|
|
157
|
+
await db.rollback()
|
|
158
|
+
raise HTTPException(
|
|
159
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
160
|
+
detail=(
|
|
161
|
+
"IntegrityError for resource deletion. "
|
|
162
|
+
f"Original error:\n{str(e)}"
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.get(
|
|
168
|
+
"/{resource_id}/profile/",
|
|
169
|
+
response_model=list[ProfileRead],
|
|
170
|
+
status_code=200,
|
|
171
|
+
)
|
|
172
|
+
async def get_resource_profiles(
|
|
173
|
+
resource_id: int,
|
|
174
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
175
|
+
db: AsyncSession = Depends(get_async_db),
|
|
176
|
+
) -> list[ProfileRead]:
|
|
177
|
+
"""
|
|
178
|
+
Query `Profile`s for single `Resource`.
|
|
179
|
+
"""
|
|
180
|
+
await _get_resource_or_404(resource_id=resource_id, db=db)
|
|
181
|
+
|
|
182
|
+
res = await db.execute(
|
|
183
|
+
select(Profile)
|
|
184
|
+
.where(Profile.resource_id == resource_id)
|
|
185
|
+
.order_by(Profile.id)
|
|
186
|
+
)
|
|
187
|
+
profiles = res.scalars().all()
|
|
188
|
+
|
|
189
|
+
return profiles
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.post(
|
|
193
|
+
"/{resource_id}/profile/",
|
|
194
|
+
response_model=ProfileRead,
|
|
195
|
+
status_code=201,
|
|
196
|
+
)
|
|
197
|
+
async def post_profile(
|
|
198
|
+
resource_id: int,
|
|
199
|
+
profile_create: ProfileCreate,
|
|
200
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
201
|
+
db: AsyncSession = Depends(get_async_db),
|
|
202
|
+
) -> ProfileRead:
|
|
203
|
+
"""
|
|
204
|
+
Create new `Profile`.
|
|
205
|
+
"""
|
|
206
|
+
resource = await _get_resource_or_404(resource_id=resource_id, db=db)
|
|
207
|
+
|
|
208
|
+
_check_resource_type_match_or_422(
|
|
209
|
+
resource=resource,
|
|
210
|
+
new_profile=profile_create,
|
|
211
|
+
)
|
|
212
|
+
await _check_profile_name(name=profile_create.name, db=db)
|
|
213
|
+
|
|
214
|
+
profile = Profile(
|
|
215
|
+
resource_id=resource_id,
|
|
216
|
+
**profile_create.model_dump(),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
db.add(profile)
|
|
220
|
+
await db.commit()
|
|
221
|
+
await db.refresh(profile)
|
|
222
|
+
return profile
|