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.
Files changed (31) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +2 -1
  3. fractal_server/app/models/security.py +7 -5
  4. fractal_server/app/routes/api/__init__.py +0 -9
  5. fractal_server/app/routes/api/v2/dataset.py +35 -9
  6. fractal_server/app/routes/api/v2/submit.py +1 -1
  7. fractal_server/app/routes/api/v2/workflowtask.py +0 -27
  8. fractal_server/app/routes/auth/current_user.py +0 -63
  9. fractal_server/app/routes/auth/group.py +1 -30
  10. fractal_server/app/routes/auth/router.py +2 -0
  11. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  12. fractal_server/app/schemas/user.py +29 -12
  13. fractal_server/app/schemas/user_group.py +0 -15
  14. fractal_server/app/security/__init__.py +1 -1
  15. fractal_server/config/__init__.py +0 -6
  16. fractal_server/config/_data.py +0 -79
  17. fractal_server/config/_main.py +5 -0
  18. fractal_server/data_migrations/2_18_1.py +29 -0
  19. fractal_server/main.py +57 -3
  20. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  21. fractal_server/runner/v2/_local.py +1 -1
  22. fractal_server/runner/v2/_slurm_ssh.py +1 -1
  23. fractal_server/runner/v2/_slurm_sudo.py +1 -1
  24. fractal_server/types/__init__.py +13 -0
  25. fractal_server/types/validators/__init__.py +1 -0
  26. fractal_server/types/validators/_common_validators.py +10 -0
  27. {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/METADATA +1 -1
  28. {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/RECORD +31 -28
  29. {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/WHEEL +0 -0
  30. {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/entry_points.txt +0 -0
  31. {fractal_server-2.18.0a1.dist-info → fractal_server-2.18.0a3.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.18.0a1"
1
+ __VERSION__ = "2.18.0a3"
@@ -167,7 +167,8 @@ def init_db_data(
167
167
  "`--admin-pwd` and `--admin-project-dir`. Exit."
168
168
  )
169
169
  sys.exit(1)
170
- if admin_password and admin_email:
170
+
171
+ if admin_email:
171
172
  asyncio.run(
172
173
  _create_first_user(
173
174
  email=admin_email,
@@ -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
- project_dir: str
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.project_dir}/fractal/"
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 (dataset_update.zarr_dir is not None) and (len(db_dataset.images) != 0):
158
- raise HTTPException(
159
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
160
- detail=(
161
- "Cannot modify `zarr_dir` because the dataset has a non-empty "
162
- "image list."
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.project_dir, FRACTAL_CACHE_DIR)
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 AbsolutePathStr
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
- project_dir: str
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
- project_dir:
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
- project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)] = (
78
- None
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
- project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)]
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
@@ -425,7 +425,7 @@ async def _create_first_user(
425
425
  kwargs = dict(
426
426
  email=email,
427
427
  password=password,
428
- project_dir=project_dir,
428
+ project_dirs=[project_dir],
429
429
  profile_id=profile_id,
430
430
  is_superuser=is_superuser,
431
431
  is_verified=is_verified,
@@ -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
@@ -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
@@ -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 `project_dir`).
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 `project_dir`).
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 `project_dir`).
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.
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fractal-server
3
- Version: 2.18.0a1
3
+ Version: 2.18.0a3
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -1,12 +1,12 @@
1
- fractal_server/__init__.py,sha256=d1wgepBuIO5PKrE5xiGZw1xlgLPm7KgGhQwIApPL-xw,25
2
- fractal_server/__main__.py,sha256=o63YYNPm6wv0YG5PTtxPselXY6g3ii5VoBP8blPGgK8,11423
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=p9h1wqWzkJ2_P1kVmsHgeA81Bt5DU24v7THCr2-J238,4671
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=kq_c4t4a0rrJ6zMO0WGOTjCHf46SAlmWhh7Sa-3LkNg,1659
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=FE5m8t8KyBQG-df_uBJIpaPHBjbHOGkFbSDhMN6fsuw,8579
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=HScEY2IC4xVLXBoe1Pwiy9bjNR1FnEuAJWjQTidEGFU,9562
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=R9yFTSfyIAfL42tNnbGUZ3CHSdko8-kSPKww9Erg4QE,9237
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=Y_2Es3HFtTMuVUmWVHFFvk3vsRgmlqS_x5BwkeavlvI,5552
66
- fractal_server/app/routes/auth/group.py,sha256=5bIM8LoUVv1C5-bMYoJbNSS8eESgVpK8-o47sUjMcZU,7293
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=-E87A8h2UvcLucy5xjzKiWbXHVKcqxUmmZGeV_utEzA,598
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=ed1kXyVoCboNiHTQSA9EVGCZNIFByFknPluehHbYKmE,2900
79
- fractal_server/app/schemas/user_group.py,sha256=uTTOVGoy89SxVDpJumjqOEWxqXWR41MNOTBDCyNxEDA,1478
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=L-Q0wsh2GjDyeiT4FfgWTyfrSH1QA52cp9zl5U5OT9I,18360
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=ZCmroNB50sUxJiFtkW0a4fFtmfyPnL4LWhtKY5FbQfg,737
101
- fractal_server/config/_data.py,sha256=M7bwGDrvTV2mIo9Y3BilzluCyEIlE-PyIjCdcTbzWZg,2804
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=s11lg9r-4hALGefG8xiMlc12ul0hL7ct4YviD-YBWWE,2230
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=6hI23zI6x3Hz1Cj7JDBNHFYVg5yCymL8oe5UyIc9T2Q,4420
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=XT6uFi0wwO5ZxR_qoszgibkUqal-pNPWWcd101NFgCg,3838
204
- fractal_server/runner/v2/_slurm_ssh.py,sha256=GcpBLvvSwadxnchS5phOXVk42BlADeDi-xr2K6FBIyU,4529
205
- fractal_server/runner/v2/_slurm_sudo.py,sha256=iexw_hpUf-jVngMRxntiKIwNT_Dvdbk2R-yHHWEFnws,4424
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=z5jzdESBld-orcOyBe6J5I-V-CQfY64nRYVFQXZUlWs,2934
259
- fractal_server/types/validators/__init__.py,sha256=5uj6KJ9MelFZgyoq3MzXLhgWCl0yiriS7XKmb0gathg,392
260
- fractal_server/types/validators/_common_validators.py,sha256=LTIQPfOPBM-KjxPgbeCLvLFwSESxAQjppzON9pt_RDQ,1148
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.0a1.dist-info/METADATA,sha256=JjghryAslh4WzJasCM1A3DpZAEolcHca19OBC9HN1T8,4277
267
- fractal_server-2.18.0a1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
268
- fractal_server-2.18.0a1.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
269
- fractal_server-2.18.0a1.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
270
- fractal_server-2.18.0a1.dist-info/RECORD,,
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,,