fractal-server 2.18.0a1__py3-none-any.whl → 2.18.0a3__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 +2 -1
- fractal_server/app/models/security.py +7 -5
- fractal_server/app/routes/api/__init__.py +0 -9
- fractal_server/app/routes/api/v2/dataset.py +35 -9
- fractal_server/app/routes/api/v2/submit.py +1 -1
- fractal_server/app/routes/api/v2/workflowtask.py +0 -27
- fractal_server/app/routes/auth/current_user.py +0 -63
- fractal_server/app/routes/auth/group.py +1 -30
- fractal_server/app/routes/auth/router.py +2 -0
- fractal_server/app/routes/auth/viewer_paths.py +43 -0
- fractal_server/app/schemas/user.py +29 -12
- fractal_server/app/schemas/user_group.py +0 -15
- fractal_server/app/security/__init__.py +1 -1
- fractal_server/config/__init__.py +0 -6
- fractal_server/config/_data.py +0 -79
- fractal_server/config/_main.py +5 -0
- fractal_server/data_migrations/2_18_1.py +29 -0
- fractal_server/main.py +57 -3
- fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
- fractal_server/runner/v2/_local.py +1 -1
- fractal_server/runner/v2/_slurm_ssh.py +1 -1
- fractal_server/runner/v2/_slurm_sudo.py +1 -1
- fractal_server/types/__init__.py +13 -0
- fractal_server/types/validators/__init__.py +1 -0
- fractal_server/types/validators/_common_validators.py +10 -0
- {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/METADATA +1 -1
- {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/RECORD +31 -28
- {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/WHEEL +0 -0
- {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/licenses/LICENSE +0 -0
fractal_server/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__VERSION__ = "2.18.
|
|
1
|
+
__VERSION__ = "2.18.0a3"
|
fractal_server/__main__.py
CHANGED
|
@@ -6,7 +6,6 @@ from pydantic import EmailStr
|
|
|
6
6
|
from sqlalchemy import Column
|
|
7
7
|
from sqlalchemy import String
|
|
8
8
|
from sqlalchemy.dialects.postgresql import ARRAY
|
|
9
|
-
from sqlalchemy.dialects.postgresql import JSONB
|
|
10
9
|
from sqlalchemy.types import DateTime
|
|
11
10
|
from sqlmodel import Field
|
|
12
11
|
from sqlmodel import Relationship
|
|
@@ -113,7 +112,13 @@ class UserOAuth(SQLModel, table=True):
|
|
|
113
112
|
ondelete="RESTRICT",
|
|
114
113
|
)
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
# TODO-2.18.1: drop `project_dir`
|
|
116
|
+
project_dir: str | None = Field(default=None, nullable=True)
|
|
117
|
+
# TODO-2.18.1: `project_dirs: list[str] = Field(min_length=1)`
|
|
118
|
+
project_dirs: list[str] = Field(
|
|
119
|
+
sa_column=Column(ARRAY(String), nullable=False, server_default="{}"),
|
|
120
|
+
)
|
|
121
|
+
|
|
117
122
|
slurm_accounts: list[str] = Field(
|
|
118
123
|
sa_column=Column(ARRAY(String), server_default="{}"),
|
|
119
124
|
)
|
|
@@ -135,6 +140,3 @@ class UserGroup(SQLModel, table=True):
|
|
|
135
140
|
default_factory=get_timestamp,
|
|
136
141
|
sa_column=Column(DateTime(timezone=True), nullable=False),
|
|
137
142
|
)
|
|
138
|
-
viewer_paths: list[str] = Field(
|
|
139
|
-
sa_column=Column(JSONB, server_default="[]", nullable=False)
|
|
140
|
-
)
|
|
@@ -8,7 +8,6 @@ from fastapi import Depends
|
|
|
8
8
|
import fractal_server
|
|
9
9
|
from fractal_server.app.models import UserOAuth
|
|
10
10
|
from fractal_server.app.routes.auth import current_superuser_act
|
|
11
|
-
from fractal_server.config import get_data_settings
|
|
12
11
|
from fractal_server.config import get_db_settings
|
|
13
12
|
from fractal_server.config import get_email_settings
|
|
14
13
|
from fractal_server.config import get_oauth_settings
|
|
@@ -50,14 +49,6 @@ async def view_email_settings(
|
|
|
50
49
|
return settings.model_dump()
|
|
51
50
|
|
|
52
51
|
|
|
53
|
-
@router_api.get("/settings/data/")
|
|
54
|
-
async def view_data_settings(
|
|
55
|
-
user: UserOAuth = Depends(current_superuser_act),
|
|
56
|
-
):
|
|
57
|
-
settings = Inject(get_data_settings)
|
|
58
|
-
return settings.model_dump()
|
|
59
|
-
|
|
60
|
-
|
|
61
52
|
@router_api.get("/settings/oauth/")
|
|
62
53
|
async def view_oauth_settings(
|
|
63
54
|
user: UserOAuth = Depends(current_superuser_act),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
from fastapi import APIRouter
|
|
2
4
|
from fastapi import Depends
|
|
3
5
|
from fastapi import HTTPException
|
|
@@ -58,7 +60,7 @@ async def create_dataset(
|
|
|
58
60
|
await db.commit()
|
|
59
61
|
await db.refresh(db_dataset)
|
|
60
62
|
path = (
|
|
61
|
-
f"{user.
|
|
63
|
+
f"{user.project_dirs[0]}/fractal/"
|
|
62
64
|
f"{project_id}_{sanitize_string(project.name)}/"
|
|
63
65
|
f"{db_dataset.id}_{sanitize_string(db_dataset.name)}"
|
|
64
66
|
)
|
|
@@ -69,6 +71,18 @@ async def create_dataset(
|
|
|
69
71
|
await db.commit()
|
|
70
72
|
await db.refresh(db_dataset)
|
|
71
73
|
else:
|
|
74
|
+
if not any(
|
|
75
|
+
Path(dataset.zarr_dir).is_relative_to(project_dir)
|
|
76
|
+
for project_dir in user.project_dirs
|
|
77
|
+
):
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
80
|
+
detail=(
|
|
81
|
+
"Dataset zarr_dir is not relative to any of the user "
|
|
82
|
+
"project directories."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
72
86
|
db_dataset = DatasetV2(project_id=project_id, **dataset.model_dump())
|
|
73
87
|
db.add(db_dataset)
|
|
74
88
|
await db.commit()
|
|
@@ -154,14 +168,26 @@ async def update_dataset(
|
|
|
154
168
|
)
|
|
155
169
|
db_dataset = output["dataset"]
|
|
156
170
|
|
|
157
|
-
if
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
if dataset_update.zarr_dir is not None:
|
|
172
|
+
if db_dataset.images:
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
175
|
+
detail=(
|
|
176
|
+
"Cannot modify `zarr_dir` because the dataset has a "
|
|
177
|
+
"non-empty image list."
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
if not any(
|
|
181
|
+
Path(dataset_update.zarr_dir).is_relative_to(project_dir)
|
|
182
|
+
for project_dir in user.project_dirs
|
|
183
|
+
):
|
|
184
|
+
raise HTTPException(
|
|
185
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
186
|
+
detail=(
|
|
187
|
+
"Dataset zarr_dir is not relative to any of the user "
|
|
188
|
+
"project directories."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
165
191
|
|
|
166
192
|
for key, value in dataset_update.model_dump(exclude_unset=True).items():
|
|
167
193
|
setattr(db_dataset, key, value)
|
|
@@ -238,7 +238,7 @@ async def apply_workflow(
|
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
# Define user-side job directory
|
|
241
|
-
cache_dir = Path(user.
|
|
241
|
+
cache_dir = Path(user.project_dirs[0], FRACTAL_CACHE_DIR)
|
|
242
242
|
match resource.type:
|
|
243
243
|
case ResourceType.LOCAL:
|
|
244
244
|
WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
|
|
@@ -5,12 +5,10 @@ from fastapi import Depends
|
|
|
5
5
|
from fastapi import HTTPException
|
|
6
6
|
from fastapi import Response
|
|
7
7
|
from fastapi import status
|
|
8
|
-
from sqlmodel import select
|
|
9
8
|
|
|
10
9
|
from fractal_server.app.db import AsyncSession
|
|
11
10
|
from fractal_server.app.db import get_async_db
|
|
12
11
|
from fractal_server.app.models import UserOAuth
|
|
13
|
-
from fractal_server.app.models.linkuserproject import LinkUserProjectV2
|
|
14
12
|
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
15
13
|
from fractal_server.app.schemas.v2 import TaskType
|
|
16
14
|
from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
|
|
@@ -53,31 +51,6 @@ async def create_workflowtask(
|
|
|
53
51
|
db=db,
|
|
54
52
|
)
|
|
55
53
|
|
|
56
|
-
res = await db.execute(
|
|
57
|
-
select(UserOAuth.id)
|
|
58
|
-
.join(LinkUserProjectV2, LinkUserProjectV2.user_id == UserOAuth.id)
|
|
59
|
-
.where(LinkUserProjectV2.project_id == project_id)
|
|
60
|
-
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
61
|
-
)
|
|
62
|
-
project_owner_id = res.scalar_one()
|
|
63
|
-
if project_owner_id != user.id:
|
|
64
|
-
try:
|
|
65
|
-
await _get_task_read_access(
|
|
66
|
-
task_id=task_id,
|
|
67
|
-
user_id=project_owner_id,
|
|
68
|
-
db=db,
|
|
69
|
-
require_active=True,
|
|
70
|
-
)
|
|
71
|
-
except HTTPException as e:
|
|
72
|
-
raise (
|
|
73
|
-
HTTPException(
|
|
74
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
75
|
-
detail="The task must be accessible to the project owner.",
|
|
76
|
-
)
|
|
77
|
-
if e.status_code == 403
|
|
78
|
-
else e
|
|
79
|
-
)
|
|
80
|
-
|
|
81
54
|
task = await _get_task_read_access(
|
|
82
55
|
task_id=task_id, user_id=user.id, db=db, require_active=True
|
|
83
56
|
)
|
|
@@ -2,21 +2,16 @@
|
|
|
2
2
|
Definition of `/auth/current-user/` endpoints
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
|
-
|
|
7
5
|
from fastapi import APIRouter
|
|
8
6
|
from fastapi import Depends
|
|
9
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
8
|
from sqlmodel import select
|
|
11
9
|
|
|
12
10
|
from fractal_server.app.db import get_async_db
|
|
13
|
-
from fractal_server.app.models import LinkUserGroup
|
|
14
11
|
from fractal_server.app.models import Profile
|
|
15
12
|
from fractal_server.app.models import Resource
|
|
16
|
-
from fractal_server.app.models import UserGroup
|
|
17
13
|
from fractal_server.app.models import UserOAuth
|
|
18
14
|
from fractal_server.app.routes.auth import current_user_act
|
|
19
|
-
from fractal_server.app.routes.auth import current_user_act_ver
|
|
20
15
|
from fractal_server.app.routes.auth._aux_auth import (
|
|
21
16
|
_get_single_user_with_groups,
|
|
22
17
|
)
|
|
@@ -26,9 +21,6 @@ from fractal_server.app.schemas.user import UserUpdate
|
|
|
26
21
|
from fractal_server.app.schemas.user import UserUpdateStrict
|
|
27
22
|
from fractal_server.app.security import UserManager
|
|
28
23
|
from fractal_server.app.security import get_user_manager
|
|
29
|
-
from fractal_server.config import DataAuthScheme
|
|
30
|
-
from fractal_server.config import get_data_settings
|
|
31
|
-
from fractal_server.syringe import Inject
|
|
32
24
|
|
|
33
25
|
router_current_user = APIRouter()
|
|
34
26
|
|
|
@@ -106,58 +98,3 @@ async def get_current_user_profile_info(
|
|
|
106
98
|
)
|
|
107
99
|
|
|
108
100
|
return response_data
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@router_current_user.get(
|
|
112
|
-
"/current-user/allowed-viewer-paths/", response_model=list[str]
|
|
113
|
-
)
|
|
114
|
-
async def get_current_user_allowed_viewer_paths(
|
|
115
|
-
current_user: UserOAuth = Depends(current_user_act_ver),
|
|
116
|
-
db: AsyncSession = Depends(get_async_db),
|
|
117
|
-
) -> list[str]:
|
|
118
|
-
"""
|
|
119
|
-
Returns the allowed viewer paths for current user, according to the
|
|
120
|
-
selected FRACTAL_DATA_AUTH_SCHEME
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
data_settings = Inject(get_data_settings)
|
|
124
|
-
|
|
125
|
-
authorized_paths = []
|
|
126
|
-
|
|
127
|
-
if data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.NONE:
|
|
128
|
-
return authorized_paths
|
|
129
|
-
|
|
130
|
-
# Append `project_dir` to the list of authorized paths
|
|
131
|
-
authorized_paths.append(current_user.project_dir)
|
|
132
|
-
|
|
133
|
-
# If auth scheme is "users-folders" and `slurm_user` is set,
|
|
134
|
-
# build and append the user folder
|
|
135
|
-
if (
|
|
136
|
-
data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.USERS_FOLDERS
|
|
137
|
-
and current_user.profile_id is not None
|
|
138
|
-
):
|
|
139
|
-
profile = await db.get(Profile, current_user.profile_id)
|
|
140
|
-
if profile is not None and profile.username is not None:
|
|
141
|
-
base_folder = data_settings.FRACTAL_DATA_BASE_FOLDER
|
|
142
|
-
user_folder = os.path.join(base_folder, profile.username)
|
|
143
|
-
authorized_paths.append(user_folder)
|
|
144
|
-
|
|
145
|
-
if data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.VIEWER_PATHS:
|
|
146
|
-
# Returns the union of `viewer_paths` for all user's groups
|
|
147
|
-
cmd = (
|
|
148
|
-
select(UserGroup.viewer_paths)
|
|
149
|
-
.join(LinkUserGroup, LinkUserGroup.group_id == UserGroup.id)
|
|
150
|
-
.where(LinkUserGroup.user_id == current_user.id)
|
|
151
|
-
)
|
|
152
|
-
res = await db.execute(cmd)
|
|
153
|
-
viewer_paths_nested = res.scalars().all()
|
|
154
|
-
|
|
155
|
-
# Flatten a nested object and make its elements unique
|
|
156
|
-
all_viewer_paths_set = {
|
|
157
|
-
path
|
|
158
|
-
for _viewer_paths in viewer_paths_nested
|
|
159
|
-
for path in _viewer_paths
|
|
160
|
-
}
|
|
161
|
-
authorized_paths.extend(all_viewer_paths_set)
|
|
162
|
-
|
|
163
|
-
return authorized_paths
|
|
@@ -16,7 +16,6 @@ from fractal_server.app.models import UserGroup
|
|
|
16
16
|
from fractal_server.app.models import UserOAuth
|
|
17
17
|
from fractal_server.app.schemas.user_group import UserGroupCreate
|
|
18
18
|
from fractal_server.app.schemas.user_group import UserGroupRead
|
|
19
|
-
from fractal_server.app.schemas.user_group import UserGroupUpdate
|
|
20
19
|
from fractal_server.config import get_settings
|
|
21
20
|
from fractal_server.logger import set_logger
|
|
22
21
|
from fractal_server.syringe import Inject
|
|
@@ -101,41 +100,13 @@ async def create_single_group(
|
|
|
101
100
|
)
|
|
102
101
|
|
|
103
102
|
# Create and return new group
|
|
104
|
-
new_group = UserGroup(
|
|
105
|
-
name=group_create.name, viewer_paths=group_create.viewer_paths
|
|
106
|
-
)
|
|
103
|
+
new_group = UserGroup(name=group_create.name)
|
|
107
104
|
db.add(new_group)
|
|
108
105
|
await db.commit()
|
|
109
106
|
|
|
110
107
|
return dict(new_group.model_dump(), user_ids=[])
|
|
111
108
|
|
|
112
109
|
|
|
113
|
-
@router_group.patch(
|
|
114
|
-
"/group/{group_id}/",
|
|
115
|
-
response_model=UserGroupRead,
|
|
116
|
-
status_code=status.HTTP_200_OK,
|
|
117
|
-
)
|
|
118
|
-
async def update_single_group(
|
|
119
|
-
group_id: int,
|
|
120
|
-
group_update: UserGroupUpdate,
|
|
121
|
-
user: UserOAuth = Depends(current_superuser_act),
|
|
122
|
-
db: AsyncSession = Depends(get_async_db),
|
|
123
|
-
) -> UserGroupRead:
|
|
124
|
-
group = await _usergroup_or_404(group_id, db)
|
|
125
|
-
|
|
126
|
-
# Patch `viewer_paths`
|
|
127
|
-
if group_update.viewer_paths is not None:
|
|
128
|
-
group.viewer_paths = group_update.viewer_paths
|
|
129
|
-
db.add(group)
|
|
130
|
-
await db.commit()
|
|
131
|
-
|
|
132
|
-
updated_group = await _get_single_usergroup_with_user_ids(
|
|
133
|
-
group_id=group_id, db=db
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
return updated_group
|
|
137
|
-
|
|
138
|
-
|
|
139
110
|
@router_group.delete("/group/{group_id}/", status_code=204)
|
|
140
111
|
async def delete_single_group(
|
|
141
112
|
group_id: int,
|
|
@@ -6,6 +6,7 @@ from .login import router_login
|
|
|
6
6
|
from .oauth import get_oauth_router
|
|
7
7
|
from .register import router_register
|
|
8
8
|
from .users import router_users
|
|
9
|
+
from .viewer_paths import router_viewer_paths
|
|
9
10
|
|
|
10
11
|
router_auth = APIRouter()
|
|
11
12
|
|
|
@@ -14,6 +15,7 @@ router_auth.include_router(router_current_user)
|
|
|
14
15
|
router_auth.include_router(router_login)
|
|
15
16
|
router_auth.include_router(router_users)
|
|
16
17
|
router_auth.include_router(router_group)
|
|
18
|
+
router_auth.include_router(router_viewer_paths)
|
|
17
19
|
router_oauth = get_oauth_router()
|
|
18
20
|
if router_oauth is not None:
|
|
19
21
|
router_auth.include_router(router_oauth)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
|
+
from sqlmodel import select
|
|
5
|
+
|
|
6
|
+
from fractal_server.app.db import get_async_db
|
|
7
|
+
from fractal_server.app.models import UserOAuth
|
|
8
|
+
from fractal_server.app.models.linkuserproject import LinkUserProjectV2
|
|
9
|
+
from fractal_server.app.models.v2.dataset import DatasetV2
|
|
10
|
+
from fractal_server.app.models.v2.project import ProjectV2
|
|
11
|
+
from fractal_server.app.routes.auth import current_user_act_ver
|
|
12
|
+
|
|
13
|
+
router_viewer_paths = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router_viewer_paths.get(
|
|
17
|
+
"/current-user/allowed-viewer-paths/", response_model=list[str]
|
|
18
|
+
)
|
|
19
|
+
async def get_current_user_allowed_viewer_paths(
|
|
20
|
+
include_shared_projects: bool = True,
|
|
21
|
+
current_user: UserOAuth = Depends(current_user_act_ver),
|
|
22
|
+
db: AsyncSession = Depends(get_async_db),
|
|
23
|
+
) -> list[str]:
|
|
24
|
+
"""
|
|
25
|
+
Returns the allowed viewer paths for current user.
|
|
26
|
+
"""
|
|
27
|
+
authorized_paths = current_user.project_dirs.copy()
|
|
28
|
+
|
|
29
|
+
if include_shared_projects:
|
|
30
|
+
res = await db.execute(
|
|
31
|
+
select(DatasetV2.zarr_dir)
|
|
32
|
+
.join(ProjectV2, ProjectV2.id == DatasetV2.project_id)
|
|
33
|
+
.join(
|
|
34
|
+
LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id
|
|
35
|
+
)
|
|
36
|
+
.where(LinkUserProjectV2.user_id == current_user.id)
|
|
37
|
+
.where(LinkUserProjectV2.is_owner.is_(False))
|
|
38
|
+
)
|
|
39
|
+
authorized_paths.extend(res.unique().scalars().all())
|
|
40
|
+
# Note that `project_dirs` and the `db.execute` result may have some
|
|
41
|
+
# common elements, and then this list may have non-unique items.
|
|
42
|
+
|
|
43
|
+
return authorized_paths
|
|
@@ -8,12 +8,18 @@ from pydantic import EmailStr
|
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
|
|
10
10
|
from fractal_server.string_tools import validate_cmd
|
|
11
|
-
from fractal_server.types import
|
|
11
|
+
from fractal_server.types import ListUniqueAbsolutePathStr
|
|
12
12
|
from fractal_server.types import ListUniqueNonEmptyString
|
|
13
13
|
from fractal_server.types import ListUniqueNonNegativeInt
|
|
14
14
|
from fractal_server.types import NonEmptyStr
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _validate_cmd_list(value: list[str]) -> list[str]:
|
|
18
|
+
for v in value:
|
|
19
|
+
validate_cmd(v)
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
class OAuthAccountRead(BaseModel):
|
|
18
24
|
"""
|
|
19
25
|
Schema for storing essential `OAuthAccount` information within
|
|
@@ -38,20 +44,17 @@ class UserRead(schemas.BaseUser[int]):
|
|
|
38
44
|
group_ids_names:
|
|
39
45
|
oauth_accounts:
|
|
40
46
|
profile_id:
|
|
47
|
+
project_dirs:
|
|
48
|
+
slurm_accounts:
|
|
41
49
|
"""
|
|
42
50
|
|
|
43
51
|
group_ids_names: list[tuple[int, str]] | None = None
|
|
44
52
|
oauth_accounts: list[OAuthAccountRead]
|
|
45
53
|
profile_id: int | None = None
|
|
46
|
-
|
|
54
|
+
project_dirs: list[str]
|
|
47
55
|
slurm_accounts: list[str]
|
|
48
56
|
|
|
49
57
|
|
|
50
|
-
def _validate_cmd(value: str) -> str:
|
|
51
|
-
validate_cmd(value)
|
|
52
|
-
return value
|
|
53
|
-
|
|
54
|
-
|
|
55
58
|
class UserUpdate(schemas.BaseUserUpdate):
|
|
56
59
|
"""
|
|
57
60
|
Schema for `User` update.
|
|
@@ -63,7 +66,7 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
63
66
|
is_superuser:
|
|
64
67
|
is_verified:
|
|
65
68
|
profile_id:
|
|
66
|
-
|
|
69
|
+
project_dirs:
|
|
67
70
|
slurm_accounts:
|
|
68
71
|
"""
|
|
69
72
|
|
|
@@ -74,9 +77,9 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
74
77
|
is_superuser: bool = None
|
|
75
78
|
is_verified: bool = None
|
|
76
79
|
profile_id: int | None = None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
project_dirs: Annotated[
|
|
81
|
+
ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
|
|
82
|
+
] = None
|
|
80
83
|
slurm_accounts: ListUniqueNonEmptyString = None
|
|
81
84
|
|
|
82
85
|
|
|
@@ -98,10 +101,14 @@ class UserCreate(schemas.BaseUserCreate):
|
|
|
98
101
|
|
|
99
102
|
Attributes:
|
|
100
103
|
profile_id:
|
|
104
|
+
project_dirs:
|
|
105
|
+
slurm_accounts:
|
|
101
106
|
"""
|
|
102
107
|
|
|
103
108
|
profile_id: int | None = None
|
|
104
|
-
|
|
109
|
+
project_dirs: Annotated[
|
|
110
|
+
ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
|
|
111
|
+
] = Field(min_length=1)
|
|
105
112
|
slurm_accounts: list[str] = Field(default_factory=list)
|
|
106
113
|
|
|
107
114
|
|
|
@@ -109,6 +116,8 @@ class UserUpdateGroups(BaseModel):
|
|
|
109
116
|
"""
|
|
110
117
|
Schema for `POST /auth/users/{user_id}/set-groups/`
|
|
111
118
|
|
|
119
|
+
Attributes:
|
|
120
|
+
group_ids:
|
|
112
121
|
"""
|
|
113
122
|
|
|
114
123
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -117,6 +126,14 @@ class UserUpdateGroups(BaseModel):
|
|
|
117
126
|
|
|
118
127
|
|
|
119
128
|
class UserProfileInfo(BaseModel):
|
|
129
|
+
"""
|
|
130
|
+
Attributes:
|
|
131
|
+
has_profile:
|
|
132
|
+
resource_name:
|
|
133
|
+
profile_name:
|
|
134
|
+
username:
|
|
135
|
+
"""
|
|
136
|
+
|
|
120
137
|
has_profile: bool
|
|
121
138
|
resource_name: str | None = None
|
|
122
139
|
profile_name: str | None = None
|
|
@@ -2,16 +2,13 @@ from datetime import datetime
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
from pydantic import ConfigDict
|
|
5
|
-
from pydantic import Field
|
|
6
5
|
from pydantic import field_serializer
|
|
7
6
|
from pydantic.types import AwareDatetime
|
|
8
7
|
|
|
9
|
-
from fractal_server.types import ListUniqueAbsolutePathStr
|
|
10
8
|
from fractal_server.types import NonEmptyStr
|
|
11
9
|
|
|
12
10
|
__all__ = (
|
|
13
11
|
"UserGroupRead",
|
|
14
|
-
"UserGroupUpdate",
|
|
15
12
|
"UserGroupCreate",
|
|
16
13
|
)
|
|
17
14
|
|
|
@@ -34,7 +31,6 @@ class UserGroupRead(BaseModel):
|
|
|
34
31
|
name: str
|
|
35
32
|
timestamp_created: AwareDatetime
|
|
36
33
|
user_ids: list[int] | None = None
|
|
37
|
-
viewer_paths: list[str]
|
|
38
34
|
|
|
39
35
|
@field_serializer("timestamp_created")
|
|
40
36
|
def serialize_datetime(v: datetime) -> str:
|
|
@@ -52,14 +48,3 @@ class UserGroupCreate(BaseModel):
|
|
|
52
48
|
model_config = ConfigDict(extra="forbid")
|
|
53
49
|
|
|
54
50
|
name: NonEmptyStr
|
|
55
|
-
viewer_paths: ListUniqueAbsolutePathStr = Field(default_factory=list)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class UserGroupUpdate(BaseModel):
|
|
59
|
-
"""
|
|
60
|
-
Schema for `UserGroup` update
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
model_config = ConfigDict(extra="forbid")
|
|
64
|
-
|
|
65
|
-
viewer_paths: ListUniqueAbsolutePathStr = None
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from ._data import DataAuthScheme # noqa F401
|
|
2
|
-
from ._data import DataSettings
|
|
3
1
|
from ._database import DatabaseSettings
|
|
4
2
|
from ._email import EmailSettings
|
|
5
3
|
from ._email import PublicEmailSettings # noqa F401
|
|
@@ -21,7 +19,3 @@ def get_email_settings(email_settings=EmailSettings()) -> EmailSettings:
|
|
|
21
19
|
|
|
22
20
|
def get_oauth_settings(oauth_settings=OAuthSettings()) -> OAuthSettings:
|
|
23
21
|
return oauth_settings
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_data_settings(data_settings=DataSettings()) -> DataSettings:
|
|
27
|
-
return data_settings
|
fractal_server/config/_data.py
CHANGED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
from enum import StrEnum
|
|
2
|
-
from typing import Self
|
|
3
|
-
|
|
4
|
-
from pydantic import model_validator
|
|
5
|
-
from pydantic_settings import BaseSettings
|
|
6
|
-
from pydantic_settings import SettingsConfigDict
|
|
7
|
-
|
|
8
|
-
from fractal_server.types import AbsolutePathStr
|
|
9
|
-
|
|
10
|
-
from ._settings_config import SETTINGS_CONFIG_DICT
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class DataAuthScheme(StrEnum):
|
|
14
|
-
VIEWER_PATHS = "viewer-paths"
|
|
15
|
-
USERS_FOLDERS = "users-folders"
|
|
16
|
-
NONE = "none"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class DataSettings(BaseSettings):
|
|
20
|
-
"""
|
|
21
|
-
Settings for the `fractal-data` integration.
|
|
22
|
-
|
|
23
|
-
See https://github.com/fractal-analytics-platform/fractal-data.
|
|
24
|
-
|
|
25
|
-
Attributes:
|
|
26
|
-
FRACTAL_DATA_AUTH_SCHEME:
|
|
27
|
-
Defines how the list of allowed viewer paths is built.
|
|
28
|
-
|
|
29
|
-
This variable affects the
|
|
30
|
-
`GET /auth/current-user/allowed-viewer-paths/` response, which is
|
|
31
|
-
then consumed by
|
|
32
|
-
[fractal-data](https://github.com/fractal-analytics-platform/fractal-data).
|
|
33
|
-
|
|
34
|
-
Options:
|
|
35
|
-
<ul>
|
|
36
|
-
<li> `"viewer-paths"`: The list of allowed viewer paths will
|
|
37
|
-
include the user's `project_dir` along with any path
|
|
38
|
-
defined in UserGroups `viewer_paths` attributes.
|
|
39
|
-
</li>
|
|
40
|
-
<li> `"users-folders"`: The list will consist of the user's
|
|
41
|
-
`project_dir` and a user-specific folder. The user folder
|
|
42
|
-
is constructed by concatenating the base folder
|
|
43
|
-
`FRACTAL_DATA_BASE_FOLDER` with the user's profile
|
|
44
|
-
`username`.
|
|
45
|
-
</li>
|
|
46
|
-
<li> `"none"`: An empty list will be returned, indicating no
|
|
47
|
-
access to viewer paths. Useful when vizarr viewer is not
|
|
48
|
-
used.
|
|
49
|
-
</li>
|
|
50
|
-
</ul>
|
|
51
|
-
FRACTAL_DATA_BASE_FOLDER:
|
|
52
|
-
Base path to Zarr files that will be served by
|
|
53
|
-
fractal-vizarr-viewer.
|
|
54
|
-
This variable is required and used only when
|
|
55
|
-
`FRACTAL_DATA_AUTHORIZATION_SCHEME` is set to `"users-folders"`.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
model_config = SettingsConfigDict(**SETTINGS_CONFIG_DICT)
|
|
59
|
-
|
|
60
|
-
FRACTAL_DATA_AUTH_SCHEME: DataAuthScheme = "none"
|
|
61
|
-
|
|
62
|
-
FRACTAL_DATA_BASE_FOLDER: AbsolutePathStr | None = None
|
|
63
|
-
|
|
64
|
-
@model_validator(mode="after")
|
|
65
|
-
def check(self: Self) -> Self:
|
|
66
|
-
"""
|
|
67
|
-
`FRACTAL_DATA_BASE_FOLDER` is required when
|
|
68
|
-
`FRACTAL_DATA_AUTHORIZATION_SCHEME` is set to `"users-folders"`.
|
|
69
|
-
"""
|
|
70
|
-
if (
|
|
71
|
-
self.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.USERS_FOLDERS
|
|
72
|
-
and self.FRACTAL_DATA_BASE_FOLDER is None
|
|
73
|
-
):
|
|
74
|
-
raise ValueError(
|
|
75
|
-
"FRACTAL_DATA_BASE_FOLDER is required when "
|
|
76
|
-
"FRACTAL_DATA_AUTH_SCHEME is set to "
|
|
77
|
-
"users-folders"
|
|
78
|
-
)
|
|
79
|
-
return self
|
fractal_server/config/_main.py
CHANGED
|
@@ -41,6 +41,10 @@ class Settings(BaseSettings):
|
|
|
41
41
|
user group (e.g. it cannot be deleted, and new users are
|
|
42
42
|
automatically added to it). If set to `None` (the default value),
|
|
43
43
|
then user groups are all equivalent, independently on their name.
|
|
44
|
+
FRACTAL_LONG_REQUEST_TIME:
|
|
45
|
+
Time limit beyond which the execution of an API request is
|
|
46
|
+
considered *slow* and an appropriate warning is logged by the
|
|
47
|
+
middleware.
|
|
44
48
|
"""
|
|
45
49
|
|
|
46
50
|
model_config = SettingsConfigDict(**SETTINGS_CONFIG_DICT)
|
|
@@ -57,3 +61,4 @@ class Settings(BaseSettings):
|
|
|
57
61
|
FRACTAL_GRACEFUL_SHUTDOWN_TIME: float = 30.0
|
|
58
62
|
FRACTAL_HELP_URL: HttpUrl | None = None
|
|
59
63
|
FRACTAL_DEFAULT_GROUP_NAME: Literal["All"] | None = None
|
|
64
|
+
FRACTAL_LONG_REQUEST_TIME: float = 30.0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.orm.attributes import flag_modified
|
|
5
|
+
from sqlmodel import select
|
|
6
|
+
|
|
7
|
+
from fractal_server.app.db import get_sync_db
|
|
8
|
+
from fractal_server.app.models import UserOAuth
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(level=logging.INFO)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fix_db():
|
|
14
|
+
logging.info("START - fix db")
|
|
15
|
+
|
|
16
|
+
with next(get_sync_db()) as db:
|
|
17
|
+
res = db.execute(select(UserOAuth).order_by(UserOAuth.email))
|
|
18
|
+
user_list = res.scalars().unique().all()
|
|
19
|
+
|
|
20
|
+
for user in user_list:
|
|
21
|
+
logging.info(f"Now handling user {user.email}.")
|
|
22
|
+
if user.project_dirs != []:
|
|
23
|
+
sys.exit(f"Non empty `project_dirs` for User[{user.id}]")
|
|
24
|
+
user.project_dirs.append(user.project_dir)
|
|
25
|
+
flag_modified(user, "project_dirs")
|
|
26
|
+
|
|
27
|
+
db.commit()
|
|
28
|
+
|
|
29
|
+
logging.info("END - fix db")
|
fractal_server/main.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import time
|
|
2
3
|
from contextlib import asynccontextmanager
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from itertools import chain
|
|
4
6
|
|
|
5
7
|
from fastapi import FastAPI
|
|
8
|
+
from starlette.types import Message
|
|
9
|
+
from starlette.types import Receive
|
|
10
|
+
from starlette.types import Scope
|
|
11
|
+
from starlette.types import Send
|
|
6
12
|
|
|
7
13
|
from fractal_server import __VERSION__
|
|
8
14
|
from fractal_server.app.schemas.v2 import ResourceType
|
|
9
15
|
|
|
10
16
|
from .app.routes.aux._runner import _backend_supports_shutdown
|
|
11
17
|
from .app.shutdown import cleanup_after_shutdown
|
|
12
|
-
from .config import get_data_settings
|
|
13
18
|
from .config import get_db_settings
|
|
14
19
|
from .config import get_email_settings
|
|
15
20
|
from .config import get_settings
|
|
@@ -54,14 +59,12 @@ def check_settings() -> None:
|
|
|
54
59
|
settings = Inject(get_settings)
|
|
55
60
|
db_settings = Inject(get_db_settings)
|
|
56
61
|
email_settings = Inject(get_email_settings)
|
|
57
|
-
data_settings = Inject(get_data_settings)
|
|
58
62
|
logger = set_logger("fractal_server_settings")
|
|
59
63
|
logger.debug("Fractal Settings:")
|
|
60
64
|
for key, value in chain(
|
|
61
65
|
db_settings.model_dump().items(),
|
|
62
66
|
settings.model_dump().items(),
|
|
63
67
|
email_settings.model_dump().items(),
|
|
64
|
-
data_settings.model_dump().items(),
|
|
65
68
|
):
|
|
66
69
|
if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
|
|
67
70
|
value = "*****"
|
|
@@ -131,6 +134,50 @@ async def lifespan(app: FastAPI):
|
|
|
131
134
|
reset_logger_handlers(logger)
|
|
132
135
|
|
|
133
136
|
|
|
137
|
+
slow_response_logger = set_logger("slow-response")
|
|
138
|
+
|
|
139
|
+
MIDDLEWARE_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SlowResponseMiddleware:
|
|
143
|
+
def __init__(self, app: FastAPI, time_threshold: float):
|
|
144
|
+
self.app = app
|
|
145
|
+
self.time_threshold = time_threshold
|
|
146
|
+
|
|
147
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
148
|
+
# Filter out any non-http scope (e.g. `type="lifespan"`)
|
|
149
|
+
if scope["type"] != "http":
|
|
150
|
+
await self.app(scope, receive, send)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Mutable variable which can be updated from within `send_wrapper`
|
|
154
|
+
context = {"status_code": None}
|
|
155
|
+
|
|
156
|
+
async def send_wrapper(message: Message):
|
|
157
|
+
if message["type"] == "http.response.start":
|
|
158
|
+
context["status_code"] = message["status"]
|
|
159
|
+
await send(message)
|
|
160
|
+
|
|
161
|
+
# Measure request time
|
|
162
|
+
start_timestamp = datetime.now()
|
|
163
|
+
start_time = time.perf_counter()
|
|
164
|
+
await self.app(scope, receive, send_wrapper)
|
|
165
|
+
stop_time = time.perf_counter()
|
|
166
|
+
request_time = stop_time - start_time
|
|
167
|
+
|
|
168
|
+
# Log if process time is too high
|
|
169
|
+
if request_time > self.time_threshold:
|
|
170
|
+
end_timestamp = datetime.now()
|
|
171
|
+
slow_response_logger.warning(
|
|
172
|
+
f"{scope['method']} {scope['route'].path}"
|
|
173
|
+
f"?{scope['query_string'].decode('utf-8')}, "
|
|
174
|
+
f"{context['status_code']}, "
|
|
175
|
+
f"{request_time:.2f}, "
|
|
176
|
+
f"{start_timestamp.strftime(MIDDLEWARE_DATETIME_FORMAT)}, "
|
|
177
|
+
f"{end_timestamp.strftime(MIDDLEWARE_DATETIME_FORMAT)}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
134
181
|
def start_application() -> FastAPI:
|
|
135
182
|
"""
|
|
136
183
|
Create the application, initialise it and collect all available routers.
|
|
@@ -140,6 +187,13 @@ def start_application() -> FastAPI:
|
|
|
140
187
|
The fully initialised application.
|
|
141
188
|
"""
|
|
142
189
|
app = FastAPI(lifespan=lifespan)
|
|
190
|
+
|
|
191
|
+
settings = Inject(get_settings)
|
|
192
|
+
app.add_middleware(
|
|
193
|
+
SlowResponseMiddleware,
|
|
194
|
+
time_threshold=settings.FRACTAL_LONG_REQUEST_TIME,
|
|
195
|
+
)
|
|
196
|
+
|
|
143
197
|
collect_routers(app)
|
|
144
198
|
return app
|
|
145
199
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""User project_dirs and UserGroup viewer paths
|
|
2
|
+
|
|
3
|
+
Revision ID: 7910eed4cf97
|
|
4
|
+
Revises: bc0e8b3327a7
|
|
5
|
+
Create Date: 2025-11-27 16:02:51.824653
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "7910eed4cf97"
|
|
15
|
+
down_revision = "bc0e8b3327a7"
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
23
|
+
batch_op.add_column(
|
|
24
|
+
sa.Column(
|
|
25
|
+
"project_dirs",
|
|
26
|
+
postgresql.ARRAY(sa.String()),
|
|
27
|
+
server_default="{}",
|
|
28
|
+
nullable=False,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
batch_op.alter_column(
|
|
32
|
+
"project_dir", existing_type=sa.VARCHAR(), nullable=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
with op.batch_alter_table("usergroup", schema=None) as batch_op:
|
|
36
|
+
batch_op.drop_column("viewer_paths")
|
|
37
|
+
|
|
38
|
+
# ### end Alembic commands ###
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def downgrade() -> None:
|
|
42
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
43
|
+
with op.batch_alter_table("usergroup", schema=None) as batch_op:
|
|
44
|
+
batch_op.add_column(
|
|
45
|
+
sa.Column(
|
|
46
|
+
"viewer_paths",
|
|
47
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
48
|
+
server_default=sa.text("'[]'::json"),
|
|
49
|
+
autoincrement=False,
|
|
50
|
+
nullable=False,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
55
|
+
batch_op.alter_column(
|
|
56
|
+
"project_dir", existing_type=sa.VARCHAR(), nullable=False
|
|
57
|
+
)
|
|
58
|
+
batch_op.drop_column("project_dirs")
|
|
59
|
+
|
|
60
|
+
# ### end Alembic commands ###
|
|
@@ -59,7 +59,7 @@ def process_workflow(
|
|
|
59
59
|
resource: Computational resource for running this job.
|
|
60
60
|
profile: Computational profile for running this job.
|
|
61
61
|
user_cache_dir:
|
|
62
|
-
User-writeable folder (typically a subfolder of `
|
|
62
|
+
User-writeable folder (typically a subfolder of `project_dirs`).
|
|
63
63
|
Only relevant for `slurm_sudo` and `slurm_ssh` backends.
|
|
64
64
|
fractal_ssh:
|
|
65
65
|
`FractalSSH` object, only relevant for the `slurm_ssh` backend.
|
|
@@ -80,7 +80,7 @@ def process_workflow(
|
|
|
80
80
|
resource: Computational resource for running this job.
|
|
81
81
|
profile: Computational profile for running this job.
|
|
82
82
|
user_cache_dir:
|
|
83
|
-
User-writeable folder (typically a subfolder of `
|
|
83
|
+
User-writeable folder (typically a subfolder of `project_dirs`).
|
|
84
84
|
Only relevant for `slurm_sudo` and `slurm_ssh` backends.
|
|
85
85
|
fractal_ssh:
|
|
86
86
|
`FractalSSH` object, only relevant for the `slurm_ssh` backend.
|
|
@@ -77,7 +77,7 @@ def process_workflow(
|
|
|
77
77
|
resource: Computational resource for running this job.
|
|
78
78
|
profile: Computational profile for running this job.
|
|
79
79
|
user_cache_dir:
|
|
80
|
-
User-writeable folder (typically a subfolder of `
|
|
80
|
+
User-writeable folder (typically a subfolder of `project_dirs`).
|
|
81
81
|
Only relevant for `slurm_sudo` and `slurm_ssh` backends.
|
|
82
82
|
fractal_ssh:
|
|
83
83
|
`FractalSSH` object, only relevant for the `slurm_ssh` backend.
|
fractal_server/types/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from pydantic.types import StringConstraints
|
|
|
9
9
|
from fractal_server.urls import normalize_url
|
|
10
10
|
|
|
11
11
|
from .validators import val_absolute_path
|
|
12
|
+
from .validators import val_canonical_path
|
|
12
13
|
from .validators import val_http_url
|
|
13
14
|
from .validators import val_unique_list
|
|
14
15
|
from .validators import valdict_keys
|
|
@@ -27,9 +28,13 @@ A non-empty string, with no leading/trailing whitespaces.
|
|
|
27
28
|
AbsolutePathStr = Annotated[
|
|
28
29
|
NonEmptyStr,
|
|
29
30
|
AfterValidator(val_absolute_path),
|
|
31
|
+
AfterValidator(val_canonical_path),
|
|
30
32
|
]
|
|
31
33
|
"""
|
|
32
34
|
String representing an absolute path.
|
|
35
|
+
|
|
36
|
+
Validation fails if the path is not absolute or if it contains a
|
|
37
|
+
parent-directory reference "/../".
|
|
33
38
|
"""
|
|
34
39
|
|
|
35
40
|
|
|
@@ -44,19 +49,27 @@ String representing an URL.
|
|
|
44
49
|
|
|
45
50
|
ZarrUrlStr = Annotated[
|
|
46
51
|
NonEmptyStr,
|
|
52
|
+
AfterValidator(val_canonical_path),
|
|
47
53
|
AfterValidator(normalize_url),
|
|
48
54
|
]
|
|
49
55
|
"""
|
|
50
56
|
String representing a zarr URL/path.
|
|
57
|
+
|
|
58
|
+
Validation fails if the path is not absolute or if it contains a
|
|
59
|
+
parent-directory reference "/../".
|
|
51
60
|
"""
|
|
52
61
|
|
|
53
62
|
|
|
54
63
|
ZarrDirStr = Annotated[
|
|
55
64
|
NonEmptyStr,
|
|
65
|
+
AfterValidator(val_canonical_path),
|
|
56
66
|
AfterValidator(normalize_url),
|
|
57
67
|
]
|
|
58
68
|
"""
|
|
59
69
|
String representing a `zarr_dir` path.
|
|
70
|
+
|
|
71
|
+
Validation fails if the path is not absolute or if it contains a
|
|
72
|
+
parent-directory reference "/../".
|
|
60
73
|
"""
|
|
61
74
|
|
|
62
75
|
DictStrAny = Annotated[
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from ._common_validators import val_absolute_path # noqa F401
|
|
2
|
+
from ._common_validators import val_canonical_path # noqa F401
|
|
2
3
|
from ._common_validators import val_http_url # noqa F401
|
|
3
4
|
from ._common_validators import val_unique_list # noqa F401
|
|
4
5
|
from ._common_validators import valdict_keys # noqa F401
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
from pydantic import HttpUrl
|
|
@@ -29,6 +30,15 @@ def val_absolute_path(path: str) -> str:
|
|
|
29
30
|
return path
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def val_canonical_path(path: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Check that a string attribute has no '/../' in it
|
|
36
|
+
"""
|
|
37
|
+
if ".." in Path(path).parts:
|
|
38
|
+
raise ValueError("String must not contain '/../'.")
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
|
|
32
42
|
def val_unique_list(must_be_unique: list) -> list:
|
|
33
43
|
if len(set(must_be_unique)) != len(must_be_unique):
|
|
34
44
|
raise ValueError("List has repetitions")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
fractal_server/__init__.py,sha256=
|
|
2
|
-
fractal_server/__main__.py,sha256=
|
|
1
|
+
fractal_server/__init__.py,sha256=YVD3Xi1NM3Zl__B7xtAU5mWok-xR764nteyhjwyFRHU,25
|
|
2
|
+
fractal_server/__main__.py,sha256=QeKoAgqoiozLJDa8kSVe-Aso1WWgrk1yLUYWS8RxZVM,11405
|
|
3
3
|
fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
|
|
4
4
|
fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
fractal_server/app/db/__init__.py,sha256=Otswoi_PlwX1zRhLTFQUKbW9Ho7piRn8dezjq8k-XaU,2834
|
|
6
6
|
fractal_server/app/models/__init__.py,sha256=oglUT1A1lLhXy2GFz3XsQ7wqkyfs3NXRtuNov-gOHXM,368
|
|
7
7
|
fractal_server/app/models/linkusergroup.py,sha256=3KkkE4QIUAlTrBAZs_tVy0pGvAxUAq6yOEjflct_z2M,678
|
|
8
8
|
fractal_server/app/models/linkuserproject.py,sha256=LW09JFTY2lFLqp3KdxUNjldeJRaao3WnTko_gS21EU4,1572
|
|
9
|
-
fractal_server/app/models/security.py,sha256=
|
|
9
|
+
fractal_server/app/models/security.py,sha256=mhKmRN5Ln2nwDOf-K_-BrSdTB6qQZrjeT_FiD76sMu4,4781
|
|
10
10
|
fractal_server/app/models/v2/__init__.py,sha256=xL05Mvdx0dqUFhJf694oPfuqkUQxZbxOkoUgRuNIXl4,949
|
|
11
11
|
fractal_server/app/models/v2/accounting.py,sha256=VNweFARrvY3mj5LI0834Ku061S2aGC61kuVHzi_tZhc,1187
|
|
12
12
|
fractal_server/app/models/v2/dataset.py,sha256=BL5elDU0UXnUSwvuXSO4JeKa9gje0QFerU_LP7sI754,1273
|
|
@@ -32,7 +32,7 @@ fractal_server/app/routes/admin/v2/sharing.py,sha256=x7RtbDPapyENEU_s4-glPoEeEOx
|
|
|
32
32
|
fractal_server/app/routes/admin/v2/task.py,sha256=DMGMUY2uF55wrCkPr3u8qBLp4UWZbAEk2W5sbYMQS-Q,6101
|
|
33
33
|
fractal_server/app/routes/admin/v2/task_group.py,sha256=CodDIzTBbTOOdLbXr7qAv6VZydpA6xl0be0W_cw42tE,9330
|
|
34
34
|
fractal_server/app/routes/admin/v2/task_group_lifecycle.py,sha256=3LtyLDLFDEWSx9e4huXV_uBUdoDuIWib7IzMjlD1OMI,9975
|
|
35
|
-
fractal_server/app/routes/api/__init__.py,sha256=
|
|
35
|
+
fractal_server/app/routes/api/__init__.py,sha256=GaNOm1elJLldHNWZ482qlvETLAXhdJ8u_X6kGxMmwQs,1409
|
|
36
36
|
fractal_server/app/routes/api/v2/__init__.py,sha256=lOSRxe408B3dUfd1FtpfynEWBwKDVFlUt3I4NpIQTRo,2938
|
|
37
37
|
fractal_server/app/routes/api/v2/_aux_functions.py,sha256=df6-Eep_402StSOiGeDHfKWZb7E1yJkFyHdi_EThRgE,15921
|
|
38
38
|
fractal_server/app/routes/api/v2/_aux_functions_history.py,sha256=RhKheO2mdSpA0PYGPuDfr8XTaE4e8LkuKeVJlilNtko,5683
|
|
@@ -41,7 +41,7 @@ fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py,sha256=u7KO6VX
|
|
|
41
41
|
fractal_server/app/routes/api/v2/_aux_functions_task_version_update.py,sha256=PKjV7r8YsPRXoNiVSnOK4KBYVV3l_Yb_ZPrqAkMkXrQ,1182
|
|
42
42
|
fractal_server/app/routes/api/v2/_aux_functions_tasks.py,sha256=jg1QUQhDmeTw6c36-gBOVqOiBgI9kqBiuU77hHaJ_ag,13627
|
|
43
43
|
fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py,sha256=vdvMTa3San1HMTzctN5Vk7zxpqe4ccByrFBQyHfgWW8,4889
|
|
44
|
-
fractal_server/app/routes/api/v2/dataset.py,sha256=
|
|
44
|
+
fractal_server/app/routes/api/v2/dataset.py,sha256=oDS8_2nzG923VnvspSud94OC1CKgqA2yPcvY5pd_aX4,9477
|
|
45
45
|
fractal_server/app/routes/api/v2/history.py,sha256=BYsfBlvcBGbJCL47KlMpZDktXAHB8ft2nIaOQMMjqPA,18183
|
|
46
46
|
fractal_server/app/routes/api/v2/images.py,sha256=k9wd44iwjCtEWSH9j6X6zToBwuOOo6J4FxSW7AGbPHA,8266
|
|
47
47
|
fractal_server/app/routes/api/v2/job.py,sha256=vRN3Ovwami_4CpZw8zJN1azltMCh2ed42dlOfVHHG6Q,7274
|
|
@@ -49,7 +49,7 @@ fractal_server/app/routes/api/v2/pre_submission_checks.py,sha256=Cs_ODoRWmkbSJJh
|
|
|
49
49
|
fractal_server/app/routes/api/v2/project.py,sha256=TlcixNdrss6-0jSiFGnlLP-qCsuX8_nhUyagG5tip3c,5833
|
|
50
50
|
fractal_server/app/routes/api/v2/sharing.py,sha256=MvegcF3xaT9nztVwLiUisp4B8IrKRa2LVlSR2GGTqYk,9398
|
|
51
51
|
fractal_server/app/routes/api/v2/status_legacy.py,sha256=zP5YheZBoeffanUpVZvKYL4kYIiGIkGtR9W9GX0pXVE,6599
|
|
52
|
-
fractal_server/app/routes/api/v2/submit.py,sha256=
|
|
52
|
+
fractal_server/app/routes/api/v2/submit.py,sha256=FpdMRCRDLGEXWPIDC6m9EcvrhysQo43fmMRMJ6UAKQY,9566
|
|
53
53
|
fractal_server/app/routes/api/v2/task.py,sha256=gekExcUmk-A8psT0_D356U-j8k38aw_wrCO8kWgG2Tw,7536
|
|
54
54
|
fractal_server/app/routes/api/v2/task_collection.py,sha256=x6TMI3JeeIJQJS7bIbn3fnRZaxIxQlp-SQHr2ZVDLBw,12384
|
|
55
55
|
fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=9Ycm8-C_bgsEgjqyRLqa64e0HwMVYZWPYr--r-oomB4,6948
|
|
@@ -59,24 +59,25 @@ fractal_server/app/routes/api/v2/task_group_lifecycle.py,sha256=g20Egnwlw8Qvm1Pz
|
|
|
59
59
|
fractal_server/app/routes/api/v2/task_version_update.py,sha256=p0Bk8B7EZi9T0IWeDPqH7U7JNcjMGj1x72-ngZGgTOU,8497
|
|
60
60
|
fractal_server/app/routes/api/v2/workflow.py,sha256=EW6u-_O6ox3DVBYgrllMfCMHC-DNetUNT6OZudbThko,10830
|
|
61
61
|
fractal_server/app/routes/api/v2/workflow_import.py,sha256=zSCWKFzwIX2fWpxZJKXHbFk11nuUrgJL50-P0E_vYTc,9668
|
|
62
|
-
fractal_server/app/routes/api/v2/workflowtask.py,sha256=
|
|
62
|
+
fractal_server/app/routes/api/v2/workflowtask.py,sha256=t4h7l63Kfr0vrD6OsN39YSDBXQyoyPqFX3hX7mYXk3U,8274
|
|
63
63
|
fractal_server/app/routes/auth/__init__.py,sha256=RghfjGuu0RTW8RxBCvaePx9KErO4rTkI96XgbtbeSJU,2337
|
|
64
64
|
fractal_server/app/routes/auth/_aux_auth.py,sha256=s_boxuhPC60j74NmE8FopYPv_Fc4hiADvL0beWPcuE0,5474
|
|
65
|
-
fractal_server/app/routes/auth/current_user.py,sha256=
|
|
66
|
-
fractal_server/app/routes/auth/group.py,sha256=
|
|
65
|
+
fractal_server/app/routes/auth/current_user.py,sha256=uDWttWo9isG69Jv1EGnnr2Ki5ZGd0D76jgjVDQMkn8c,3251
|
|
66
|
+
fractal_server/app/routes/auth/group.py,sha256=uR98vdQHH-7BFl-Czj85ESPxT2yQymy4qtagaMrnUPU,6491
|
|
67
67
|
fractal_server/app/routes/auth/login.py,sha256=buVa5Y8T0cd_SW1CqC-zMv-3SfPxGJknf7MYlUyKOl0,567
|
|
68
68
|
fractal_server/app/routes/auth/oauth.py,sha256=NxrwOWBGPe7hLPEnD66nfWPGMWzDM80LIrwtmONVw-4,2731
|
|
69
69
|
fractal_server/app/routes/auth/register.py,sha256=IiUJhgY0ZrTs0RlBRRjoTv4wF5Gb3eXTInFV-dXkpsE,615
|
|
70
|
-
fractal_server/app/routes/auth/router.py,sha256
|
|
70
|
+
fractal_server/app/routes/auth/router.py,sha256=Zip_fw9qJWtoXWjluznschyrCKb2n_rf3xWarSXMkgI,692
|
|
71
71
|
fractal_server/app/routes/auth/users.py,sha256=2E1TEWRGprp-TyF39VJ3Bu6p9tyy37xObG0Ijqa1aHg,7089
|
|
72
|
+
fractal_server/app/routes/auth/viewer_paths.py,sha256=pzlZ1Gd_CDHVCD9ysJPfJwSbwemSab2wuEfHg0dKnxI,1616
|
|
72
73
|
fractal_server/app/routes/aux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
74
|
fractal_server/app/routes/aux/_job.py,sha256=n-UhONvomKyKkQDDqd0lFh2kCMhlCGXpfdMNW39R1E4,644
|
|
74
75
|
fractal_server/app/routes/aux/_runner.py,sha256=-SvcXCVEV7Mb6q4PbbxuTCCruX6sAlR5QGXk9CzBVv8,979
|
|
75
76
|
fractal_server/app/routes/aux/validate_user_profile.py,sha256=fGqJDdAFkbQoEIjqZ5F9-SDY_4os63R2EUMqODC7eBg,1969
|
|
76
77
|
fractal_server/app/routes/pagination.py,sha256=C4XW6cnyDfyu1XMHXRN4wgk72lsS0UtlINZmwGZFb4Y,1174
|
|
77
78
|
fractal_server/app/schemas/__init__.py,sha256=VIWJCaqokte3OljDLX00o-EC2d12rFoPb5HOLKQI94Y,86
|
|
78
|
-
fractal_server/app/schemas/user.py,sha256=
|
|
79
|
-
fractal_server/app/schemas/user_group.py,sha256=
|
|
79
|
+
fractal_server/app/schemas/user.py,sha256=6-UoImeMCcmu6-CJ4CYNNCVuB3s99wpRYn5PFgIc6Bg,3265
|
|
80
|
+
fractal_server/app/schemas/user_group.py,sha256=irel29GbffKCXNcyrAYbNSN3pCgmoUQ1wG32_s6jvos,1082
|
|
80
81
|
fractal_server/app/schemas/v2/__init__.py,sha256=mMhdt4Jo-lyG8bITGAbpuyhDJx4MNgk-VSTKqcE6Ymo,4101
|
|
81
82
|
fractal_server/app/schemas/v2/accounting.py,sha256=6EVUdPTkFY6Wb9-Vc0cIEZYVXwGEvJ3tP4YOXYE1hao,546
|
|
82
83
|
fractal_server/app/schemas/v2/dataset.py,sha256=cBsEkny-EgNqFETMGRbJu5ChfYOnsKhkivqXK5dEOxQ,1938
|
|
@@ -94,16 +95,17 @@ fractal_server/app/schemas/v2/task_collection.py,sha256=QUiMAwckHSzjXlC_cyNSR1QX
|
|
|
94
95
|
fractal_server/app/schemas/v2/task_group.py,sha256=4hNZUXnWYSozpLXR3JqBvGzfZBG2TbjqydckHHu2Aq0,3506
|
|
95
96
|
fractal_server/app/schemas/v2/workflow.py,sha256=L-dW6SzCH_VNoH6ENip44lTgGGqVYHHBk_3PtM-Ooy8,1772
|
|
96
97
|
fractal_server/app/schemas/v2/workflowtask.py,sha256=ckfPmbPTAn0lzbiaWQItdGxEUuWsRxiWBj898VEb1gw,3640
|
|
97
|
-
fractal_server/app/security/__init__.py,sha256=
|
|
98
|
+
fractal_server/app/security/__init__.py,sha256=sblIH9DFCt_iyk22WzV6k4LuKdbvNPtS1HqPCHIiBJ4,18363
|
|
98
99
|
fractal_server/app/security/signup_email.py,sha256=ZnwwjpL6jyIkegHBebTqYYGGVtllLI0_x48K-yJtkNk,1969
|
|
99
100
|
fractal_server/app/shutdown.py,sha256=QU4DfNvqwUXlHiLORtYJit4DxlFQo014SKTfs4dcE2U,2295
|
|
100
|
-
fractal_server/config/__init__.py,sha256=
|
|
101
|
-
fractal_server/config/_data.py,sha256=
|
|
101
|
+
fractal_server/config/__init__.py,sha256=WvcoE3qiY1qnkumv3qspcemCFw5iFG5NkSFR78vN4ks,562
|
|
102
|
+
fractal_server/config/_data.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
102
103
|
fractal_server/config/_database.py,sha256=0_FvboMkQEKKRxvr9uFdp98oiQwMTFbdCW3loTZNSY0,1846
|
|
103
104
|
fractal_server/config/_email.py,sha256=vMwLHN9-beYp_-up-WkTpeyNUZk4EHwt3N2l6-PYnx4,4364
|
|
104
|
-
fractal_server/config/_main.py,sha256=
|
|
105
|
+
fractal_server/config/_main.py,sha256=H2VbUsQEb4lnwe-ReHqHrb3IVbTdxxBUtbXkVlQ6JzY,2478
|
|
105
106
|
fractal_server/config/_oauth.py,sha256=UTmlFppDZcOQhr3RvkiG5XMqvr54XRAQ_Y-iR0V8N-8,2024
|
|
106
107
|
fractal_server/config/_settings_config.py,sha256=tsyXQOnn9QKCFJD6hRo_dJXlQQyl70DbqgHMJoZ1xnY,144
|
|
108
|
+
fractal_server/data_migrations/2_18_1.py,sha256=swGQGMOh5HzB09CVWVpk_a0OoksuYKQq-_MuWK89WME,815
|
|
107
109
|
fractal_server/data_migrations/README.md,sha256=_3AEFvDg9YkybDqCLlFPdDmGJvr6Tw7HRI14aZ3LOIw,398
|
|
108
110
|
fractal_server/data_migrations/tools.py,sha256=LeMeASwYGtEqd-3wOLle6WARdTGAimoyMmRbbJl-hAM,572
|
|
109
111
|
fractal_server/exceptions.py,sha256=l6aZDk_6u_9PwDaQSoIFdI40ekpzqOJaxjx5rhW-HVI,141
|
|
@@ -113,7 +115,7 @@ fractal_server/images/models.py,sha256=dNcCW7XzRRbqL86LJ5aGc6LUAqIPsZXMq67IZyGbI
|
|
|
113
115
|
fractal_server/images/status_tools.py,sha256=Is2QWThbLCrVJuI0NpGv7TcWs1T8z8q_8Qsidr3TdBU,4932
|
|
114
116
|
fractal_server/images/tools.py,sha256=37jVIU6RiAGbiyucNDlKe9J3yN3Y47NOvv-RJor9Jm0,4154
|
|
115
117
|
fractal_server/logger.py,sha256=9EhRdgPnGdbJ51vxhOD42K0iaDRhKx7wuikpHoh9kzY,5302
|
|
116
|
-
fractal_server/main.py,sha256=
|
|
118
|
+
fractal_server/main.py,sha256=CrPlBe95ng1nYiVRRl_F5qe6h0W_YTVt_H0zXXJJf8c,6255
|
|
117
119
|
fractal_server/migrations/env.py,sha256=nfyBpMIOT3kny6t-b-tUjyRjZ4k906bb1_wCQ7me1BI,1353
|
|
118
120
|
fractal_server/migrations/naming_convention.py,sha256=bSEMiMZeArmWKrUk-12lhnOw1pAFMg6LEl7yucohPqc,263
|
|
119
121
|
fractal_server/migrations/versions/034a469ec2eb_task_groups.py,sha256=uuf0sJibC4Am1HDb_dX_Jdj2oinptlg2ojiHwCpjDCY,6155
|
|
@@ -135,6 +137,7 @@ fractal_server/migrations/versions/5bf02391cfef_v2.py,sha256=jTNyZ8H5VDh4eRvCEy-
|
|
|
135
137
|
fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py,sha256=vJ6nDb7UnkCMIPg2zNM7ZE0JOTvaqFL3Fe9UarP-ivM,1633
|
|
136
138
|
fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py,sha256=qpHZC97AduFk5_G0xHs9akhnhpzb1LZooYCTPHy7n28,1353
|
|
137
139
|
fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py,sha256=PwTfY9Kq3_cwb5G4E0sM9u7UjzOhOwsYCspymmPgipQ,795
|
|
140
|
+
fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py,sha256=FvX6dq0DsbKmF-M1TZ63b6lpPGCiJev88sVmQOU80M4,1730
|
|
138
141
|
fractal_server/migrations/versions/791ce783d3d8_add_indices.py,sha256=IeWDVBRryFcFYIJVXhj-QJ6jczGnN_41K7sh6RYF0C4,1154
|
|
139
142
|
fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py,sha256=bLFmGJF7jLkices6PJFO1pmit_4-bu8rwykR6ZWDiCQ,6378
|
|
140
143
|
fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py,sha256=mOlqERL0MrF6Inp4onoSB2mAnyGeh4gW_IysCeCHTj4,2685
|
|
@@ -200,9 +203,9 @@ fractal_server/runner/filenames.py,sha256=lPnxKHtdRizr6FqG3zOdjDPyWA7GoaJGTtiuJV
|
|
|
200
203
|
fractal_server/runner/set_start_and_last_task_index.py,sha256=NsioSzfEpGyo9ZKrV5KsbxeI7d5V3tE678Y3IAo5rHM,1218
|
|
201
204
|
fractal_server/runner/task_files.py,sha256=n54A1x0MQRGSgqhzOTE-TPzEGJymUhQIUV9ApcVCV9M,4318
|
|
202
205
|
fractal_server/runner/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
203
|
-
fractal_server/runner/v2/_local.py,sha256=
|
|
204
|
-
fractal_server/runner/v2/_slurm_ssh.py,sha256=
|
|
205
|
-
fractal_server/runner/v2/_slurm_sudo.py,sha256=
|
|
206
|
+
fractal_server/runner/v2/_local.py,sha256=BzHnl6vc9KlHEEW_ObaBtP33JS1NXVu-KF6pmsodVgI,3839
|
|
207
|
+
fractal_server/runner/v2/_slurm_ssh.py,sha256=8MLn5ux2IQ_a4Sti0pajBkduSp0pwxvEJckoUXqfBZY,4530
|
|
208
|
+
fractal_server/runner/v2/_slurm_sudo.py,sha256=mMGqlhhbQLHwniiTzJwxUCLKPEMbYASQ4DL-exszQ2Y,4425
|
|
206
209
|
fractal_server/runner/v2/db_tools.py,sha256=twqFWVENkxWCYglb__BAXASDuJppwHE-VxdEUC67mq0,3317
|
|
207
210
|
fractal_server/runner/v2/deduplicate_list.py,sha256=TWxHDucal0VZPswy_H7IFaEb4ddGnpl_QBwJ8g9Ybug,668
|
|
208
211
|
fractal_server/runner/v2/merge_outputs.py,sha256=0ahaSwdMFAoEhxVaEaO9nSJuKIcWg9pDZ356ktSHcC0,897
|
|
@@ -255,16 +258,16 @@ fractal_server/tasks/v2/utils_package_names.py,sha256=-FAcbwBHsjyvhIK0QKue9_0xJU
|
|
|
255
258
|
fractal_server/tasks/v2/utils_pixi.py,sha256=Z0FnRqVynSvXDDeFL0anz7zwKrBDLGdQxyuJdipt2DI,3411
|
|
256
259
|
fractal_server/tasks/v2/utils_python_interpreter.py,sha256=36AvrMoydr9w6Rm_7hKl5QK8zYI0KIm4Pv8WHANWwjE,658
|
|
257
260
|
fractal_server/tasks/v2/utils_templates.py,sha256=L5GblhIKJwyzUbCORj1et5mh-7mG19nT5kmIpxOEj90,3489
|
|
258
|
-
fractal_server/types/__init__.py,sha256=
|
|
259
|
-
fractal_server/types/validators/__init__.py,sha256=
|
|
260
|
-
fractal_server/types/validators/_common_validators.py,sha256=
|
|
261
|
+
fractal_server/types/__init__.py,sha256=NU70aH1wiUKaMOU2Fx0vPqVaWhM6vqCtwkRtfnCtMzs,3400
|
|
262
|
+
fractal_server/types/validators/__init__.py,sha256=tkwLSeI_KQs0mEQSs6PRPBAVTOzYf2nAISQLeQVSBAw,456
|
|
263
|
+
fractal_server/types/validators/_common_validators.py,sha256=EUMYJiTGjxp022dNIBCzhj2ckUn6Zl02L_WwRAUQO_I,1396
|
|
261
264
|
fractal_server/types/validators/_filter_validators.py,sha256=irmjzycmiR6F4fmWUeA45Pdh7AeLufwVjNItskDsknk,831
|
|
262
265
|
fractal_server/types/validators/_workflow_task_arguments_validators.py,sha256=zt4TQBiLiNVD3yMYbN-dkX0AWRAjG4vRv3FIybh9zLQ,372
|
|
263
266
|
fractal_server/urls.py,sha256=QjIKAC1a46bCdiPMu3AlpgFbcv6a4l3ABcd5xz190Og,471
|
|
264
267
|
fractal_server/utils.py,sha256=-rjg8QTXQcKweXjn0NcmETFs1_uM9PGnbl0Q7c4ERPM,2181
|
|
265
268
|
fractal_server/zip_tools.py,sha256=Uhn-ax4_9g1PJ32BdyaX30hFpAeVOv2tZYTUK-zVn1E,5719
|
|
266
|
-
fractal_server-2.18.
|
|
267
|
-
fractal_server-2.18.
|
|
268
|
-
fractal_server-2.18.
|
|
269
|
-
fractal_server-2.18.
|
|
270
|
-
fractal_server-2.18.
|
|
269
|
+
fractal_server-2.18.0a3.dist-info/METADATA,sha256=2LQOCxz4cW7IElYNF6K7Qm-Y1HQgGAuss3Nsrtyd2nw,4277
|
|
270
|
+
fractal_server-2.18.0a3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
271
|
+
fractal_server-2.18.0a3.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
|
|
272
|
+
fractal_server-2.18.0a3.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
|
|
273
|
+
fractal_server-2.18.0a3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|