fractal-server 2.9.0a9__py3-none-any.whl → 2.9.0a11__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/app/routes/admin/v2/task.py +13 -0
- fractal_server/app/routes/auth/group.py +47 -39
- fractal_server/app/routes/auth/users.py +97 -99
- fractal_server/app/schemas/user.py +16 -10
- fractal_server/app/schemas/user_group.py +0 -11
- {fractal_server-2.9.0a9.dist-info → fractal_server-2.9.0a11.dist-info}/METADATA +1 -1
- {fractal_server-2.9.0a9.dist-info → fractal_server-2.9.0a11.dist-info}/RECORD +11 -11
- {fractal_server-2.9.0a9.dist-info → fractal_server-2.9.0a11.dist-info}/LICENSE +0 -0
- {fractal_server-2.9.0a9.dist-info → fractal_server-2.9.0a11.dist-info}/WHEEL +0 -0
- {fractal_server-2.9.0a9.dist-info → fractal_server-2.9.0a11.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.9.
|
1
|
+
__VERSION__ = "2.9.0a11"
|
@@ -7,6 +7,7 @@ from fastapi import status
|
|
7
7
|
from pydantic import BaseModel
|
8
8
|
from pydantic import EmailStr
|
9
9
|
from pydantic import Field
|
10
|
+
from sqlmodel import func
|
10
11
|
from sqlmodel import select
|
11
12
|
|
12
13
|
from fractal_server.app.db import AsyncSession
|
@@ -60,6 +61,9 @@ async def query_tasks(
|
|
60
61
|
version: Optional[str] = None,
|
61
62
|
name: Optional[str] = None,
|
62
63
|
max_number_of_results: int = 25,
|
64
|
+
category: Optional[str] = None,
|
65
|
+
modality: Optional[str] = None,
|
66
|
+
author: Optional[str] = None,
|
63
67
|
user: UserOAuth = Depends(current_active_superuser),
|
64
68
|
db: AsyncSession = Depends(get_async_db),
|
65
69
|
) -> list[TaskV2Info]:
|
@@ -74,6 +78,9 @@ async def query_tasks(
|
|
74
78
|
version: If not `None`, query for matching `task.version`.
|
75
79
|
name: If not `None`, query for contained case insensitive `task.name`.
|
76
80
|
max_number_of_results: The maximum length of the response.
|
81
|
+
category:
|
82
|
+
modality:
|
83
|
+
author:
|
77
84
|
"""
|
78
85
|
|
79
86
|
stm = select(TaskV2)
|
@@ -86,6 +93,12 @@ async def query_tasks(
|
|
86
93
|
stm = stm.where(TaskV2.version == version)
|
87
94
|
if name is not None:
|
88
95
|
stm = stm.where(TaskV2.name.icontains(name))
|
96
|
+
if category is not None:
|
97
|
+
stm = stm.where(func.lower(TaskV2.category) == category.lower())
|
98
|
+
if modality is not None:
|
99
|
+
stm = stm.where(func.lower(TaskV2.modality) == modality.lower())
|
100
|
+
if author is not None:
|
101
|
+
stm = stm.where(TaskV2.authors.icontains(author))
|
89
102
|
|
90
103
|
res = await db.execute(stm)
|
91
104
|
task_list = res.scalars().all()
|
@@ -6,14 +6,12 @@ from fastapi import Depends
|
|
6
6
|
from fastapi import HTTPException
|
7
7
|
from fastapi import Response
|
8
8
|
from fastapi import status
|
9
|
-
from sqlalchemy.exc import IntegrityError
|
10
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
11
|
-
from sqlmodel import col
|
12
|
-
from sqlmodel import func
|
13
10
|
from sqlmodel import select
|
14
11
|
|
15
12
|
from . import current_active_superuser
|
16
13
|
from ._aux_auth import _get_single_usergroup_with_user_ids
|
14
|
+
from ._aux_auth import _user_or_404
|
17
15
|
from ._aux_auth import _usergroup_or_404
|
18
16
|
from fractal_server.app.db import get_async_db
|
19
17
|
from fractal_server.app.models import LinkUserGroup
|
@@ -126,42 +124,6 @@ async def update_single_group(
|
|
126
124
|
|
127
125
|
group = await _usergroup_or_404(group_id, db)
|
128
126
|
|
129
|
-
# Check that all required users exist
|
130
|
-
# Note: The reason for introducing `col` is as in
|
131
|
-
# https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
|
132
|
-
stm = select(func.count()).where(
|
133
|
-
col(UserOAuth.id).in_(group_update.new_user_ids)
|
134
|
-
)
|
135
|
-
res = await db.execute(stm)
|
136
|
-
number_matching_users = res.scalar()
|
137
|
-
if number_matching_users != len(group_update.new_user_ids):
|
138
|
-
raise HTTPException(
|
139
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
140
|
-
detail=(
|
141
|
-
f"Not all requested users (IDs {group_update.new_user_ids}) "
|
142
|
-
"exist."
|
143
|
-
),
|
144
|
-
)
|
145
|
-
|
146
|
-
# Add new users to existing group
|
147
|
-
for user_id in group_update.new_user_ids:
|
148
|
-
link = LinkUserGroup(user_id=user_id, group_id=group_id)
|
149
|
-
db.add(link)
|
150
|
-
try:
|
151
|
-
await db.commit()
|
152
|
-
except IntegrityError as e:
|
153
|
-
error_msg = (
|
154
|
-
f"Cannot link users with IDs {group_update.new_user_ids} "
|
155
|
-
f"to group {group_id}. "
|
156
|
-
"Likely reason: one of these links already exists.\n"
|
157
|
-
f"Original error: {str(e)}"
|
158
|
-
)
|
159
|
-
logger.info(error_msg)
|
160
|
-
raise HTTPException(
|
161
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
162
|
-
detail=error_msg,
|
163
|
-
)
|
164
|
-
|
165
127
|
# Patch `viewer_paths`
|
166
128
|
if group_update.viewer_paths is not None:
|
167
129
|
group.viewer_paths = group_update.viewer_paths
|
@@ -239,3 +201,49 @@ async def patch_user_settings_bulk(
|
|
239
201
|
await db.commit()
|
240
202
|
|
241
203
|
return Response(status_code=status.HTTP_200_OK)
|
204
|
+
|
205
|
+
|
206
|
+
@router_group.post("/group/{group_id}/add-user/{user_id}/", status_code=200)
|
207
|
+
async def add_user_to_group(
|
208
|
+
group_id: int,
|
209
|
+
user_id: int,
|
210
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
211
|
+
db: AsyncSession = Depends(get_async_db),
|
212
|
+
) -> UserGroupRead:
|
213
|
+
await _usergroup_or_404(group_id, db)
|
214
|
+
user = await _user_or_404(user_id, db)
|
215
|
+
link = await db.get(LinkUserGroup, (group_id, user_id))
|
216
|
+
if link is None:
|
217
|
+
db.add(LinkUserGroup(group_id=group_id, user_id=user_id))
|
218
|
+
await db.commit()
|
219
|
+
else:
|
220
|
+
raise HTTPException(
|
221
|
+
status_code=422,
|
222
|
+
detail=(
|
223
|
+
f"User '{user.email}' is already a member of group {group_id}."
|
224
|
+
),
|
225
|
+
)
|
226
|
+
group = await _get_single_usergroup_with_user_ids(group_id=group_id, db=db)
|
227
|
+
return group
|
228
|
+
|
229
|
+
|
230
|
+
@router_group.post("/group/{group_id}/remove-user/{user_id}/", status_code=200)
|
231
|
+
async def remove_user_from_group(
|
232
|
+
group_id: int,
|
233
|
+
user_id: int,
|
234
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
235
|
+
db: AsyncSession = Depends(get_async_db),
|
236
|
+
) -> UserGroupRead:
|
237
|
+
await _usergroup_or_404(group_id, db)
|
238
|
+
user = await _user_or_404(user_id, db)
|
239
|
+
link = await db.get(LinkUserGroup, (group_id, user_id))
|
240
|
+
if link is None:
|
241
|
+
raise HTTPException(
|
242
|
+
status_code=422,
|
243
|
+
detail=f"User '{user.email}' is not a member of group {group_id}.",
|
244
|
+
)
|
245
|
+
else:
|
246
|
+
await db.delete(link)
|
247
|
+
await db.commit()
|
248
|
+
group = await _get_single_usergroup_with_user_ids(group_id=group_id, db=db)
|
249
|
+
return group
|
@@ -8,9 +8,7 @@ from fastapi import status
|
|
8
8
|
from fastapi_users import exceptions
|
9
9
|
from fastapi_users import schemas
|
10
10
|
from fastapi_users.router.common import ErrorCode
|
11
|
-
from sqlalchemy.exc import IntegrityError
|
12
11
|
from sqlalchemy.ext.asyncio import AsyncSession
|
13
|
-
from sqlmodel import col
|
14
12
|
from sqlmodel import func
|
15
13
|
from sqlmodel import select
|
16
14
|
|
@@ -18,9 +16,10 @@ from . import current_active_superuser
|
|
18
16
|
from ...db import get_async_db
|
19
17
|
from ...schemas.user import UserRead
|
20
18
|
from ...schemas.user import UserUpdate
|
21
|
-
from ...schemas.user import UserUpdateWithNewGroupIds
|
22
19
|
from ..aux.validate_user_settings import verify_user_has_settings
|
20
|
+
from ._aux_auth import _get_default_usergroup_id
|
23
21
|
from ._aux_auth import _get_single_user_with_groups
|
22
|
+
from ._aux_auth import FRACTAL_DEFAULT_GROUP_NAME
|
24
23
|
from fractal_server.app.models import LinkUserGroup
|
25
24
|
from fractal_server.app.models import UserGroup
|
26
25
|
from fractal_server.app.models import UserOAuth
|
@@ -28,6 +27,7 @@ from fractal_server.app.models import UserSettings
|
|
28
27
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
29
28
|
from fractal_server.app.schemas import UserSettingsRead
|
30
29
|
from fractal_server.app.schemas import UserSettingsUpdate
|
30
|
+
from fractal_server.app.schemas.user import UserUpdateGroups
|
31
31
|
from fractal_server.app.security import get_user_manager
|
32
32
|
from fractal_server.app.security import UserManager
|
33
33
|
from fractal_server.logger import set_logger
|
@@ -55,114 +55,43 @@ async def get_user(
|
|
55
55
|
@router_users.patch("/users/{user_id}/", response_model=UserRead)
|
56
56
|
async def patch_user(
|
57
57
|
user_id: int,
|
58
|
-
user_update:
|
58
|
+
user_update: UserUpdate,
|
59
59
|
current_superuser: UserOAuth = Depends(current_active_superuser),
|
60
60
|
user_manager: UserManager = Depends(get_user_manager),
|
61
61
|
db: AsyncSession = Depends(get_async_db),
|
62
62
|
):
|
63
63
|
"""
|
64
64
|
Custom version of the PATCH-user route from `fastapi-users`.
|
65
|
-
|
66
|
-
In order to keep the fastapi-users logic in place (which is convenient to
|
67
|
-
update user attributes), we split the endpoint into two branches. We either
|
68
|
-
go through the fastapi-users-based attribute-update branch, or through the
|
69
|
-
branch where we establish new user/group relationships.
|
70
|
-
|
71
|
-
Note that we prevent making both changes at the same time, since it would
|
72
|
-
be more complex to guarantee that endpoint error would leave the database
|
73
|
-
in the same state as before the API call.
|
74
65
|
"""
|
75
66
|
|
76
|
-
# We prevent simultaneous editing of both user attributes and user/group
|
77
|
-
# associations
|
78
|
-
user_update_dict_without_groups = user_update.dict(
|
79
|
-
exclude_unset=True, exclude={"new_group_ids"}
|
80
|
-
)
|
81
|
-
edit_attributes = user_update_dict_without_groups != {}
|
82
|
-
edit_groups = user_update.new_group_ids is not None
|
83
|
-
if edit_attributes and edit_groups:
|
84
|
-
raise HTTPException(
|
85
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
86
|
-
detail=(
|
87
|
-
"Cannot modify both user attributes and group membership. "
|
88
|
-
"Please make two independent PATCH calls"
|
89
|
-
),
|
90
|
-
)
|
91
|
-
|
92
67
|
# Check that user exists
|
93
68
|
user_to_patch = await _user_or_404(user_id, db)
|
94
69
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
70
|
+
# Modify user attributes
|
71
|
+
try:
|
72
|
+
user = await user_manager.update(
|
73
|
+
user_update,
|
74
|
+
user_to_patch,
|
75
|
+
safe=False,
|
76
|
+
request=None,
|
77
|
+
)
|
78
|
+
validated_user = schemas.model_validate(UserOAuth, user)
|
79
|
+
patched_user = await db.get(
|
80
|
+
UserOAuth, validated_user.id, populate_existing=True
|
81
|
+
)
|
82
|
+
except exceptions.InvalidPasswordException as e:
|
83
|
+
raise HTTPException(
|
84
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
85
|
+
detail={
|
86
|
+
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
87
|
+
"reason": e.reason,
|
88
|
+
},
|
89
|
+
)
|
90
|
+
except exceptions.UserAlreadyExists:
|
91
|
+
raise HTTPException(
|
92
|
+
status.HTTP_400_BAD_REQUEST,
|
93
|
+
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
|
103
94
|
)
|
104
|
-
res = await db.execute(stm)
|
105
|
-
number_matching_groups = res.scalar()
|
106
|
-
if number_matching_groups != len(user_update.new_group_ids):
|
107
|
-
raise HTTPException(
|
108
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
109
|
-
detail=(
|
110
|
-
"Not all requested groups (IDs: "
|
111
|
-
f"{user_update.new_group_ids}) exist."
|
112
|
-
),
|
113
|
-
)
|
114
|
-
|
115
|
-
for new_group_id in user_update.new_group_ids:
|
116
|
-
link = LinkUserGroup(user_id=user_id, group_id=new_group_id)
|
117
|
-
db.add(link)
|
118
|
-
|
119
|
-
try:
|
120
|
-
await db.commit()
|
121
|
-
except IntegrityError as e:
|
122
|
-
error_msg = (
|
123
|
-
f"Cannot link groups with IDs {user_update.new_group_ids} "
|
124
|
-
f"to user {user_id}. "
|
125
|
-
"Likely reason: one of these links already exists.\n"
|
126
|
-
f"Original error: {str(e)}"
|
127
|
-
)
|
128
|
-
logger.info(error_msg)
|
129
|
-
raise HTTPException(
|
130
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
131
|
-
detail=error_msg,
|
132
|
-
)
|
133
|
-
patched_user = user_to_patch
|
134
|
-
elif edit_attributes:
|
135
|
-
# Modify user attributes
|
136
|
-
try:
|
137
|
-
user_update_without_groups = UserUpdate(
|
138
|
-
**user_update_dict_without_groups
|
139
|
-
)
|
140
|
-
user = await user_manager.update(
|
141
|
-
user_update_without_groups,
|
142
|
-
user_to_patch,
|
143
|
-
safe=False,
|
144
|
-
request=None,
|
145
|
-
)
|
146
|
-
validated_user = schemas.model_validate(UserOAuth, user)
|
147
|
-
patched_user = await db.get(
|
148
|
-
UserOAuth, validated_user.id, populate_existing=True
|
149
|
-
)
|
150
|
-
except exceptions.InvalidPasswordException as e:
|
151
|
-
raise HTTPException(
|
152
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
153
|
-
detail={
|
154
|
-
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
155
|
-
"reason": e.reason,
|
156
|
-
},
|
157
|
-
)
|
158
|
-
except exceptions.UserAlreadyExists:
|
159
|
-
raise HTTPException(
|
160
|
-
status.HTTP_400_BAD_REQUEST,
|
161
|
-
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
|
162
|
-
)
|
163
|
-
else:
|
164
|
-
# Nothing to do, just continue
|
165
|
-
patched_user = user_to_patch
|
166
95
|
|
167
96
|
# Enrich user object with `group_ids_names` attribute
|
168
97
|
patched_user_with_groups = await _get_single_user_with_groups(
|
@@ -203,6 +132,75 @@ async def list_users(
|
|
203
132
|
return user_list
|
204
133
|
|
205
134
|
|
135
|
+
@router_users.post("/users/{user_id}/set-groups/", response_model=UserRead)
|
136
|
+
async def set_user_groups(
|
137
|
+
user_id: int,
|
138
|
+
user_update: UserUpdateGroups,
|
139
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
140
|
+
db: AsyncSession = Depends(get_async_db),
|
141
|
+
) -> UserRead:
|
142
|
+
|
143
|
+
# Preliminary check that all objects exist in the db
|
144
|
+
user = await _user_or_404(user_id=user_id, db=db)
|
145
|
+
target_group_ids = user_update.group_ids
|
146
|
+
stm = select(func.count(UserGroup.id)).where(
|
147
|
+
UserGroup.id.in_(target_group_ids)
|
148
|
+
)
|
149
|
+
res = await db.execute(stm)
|
150
|
+
count = res.scalar()
|
151
|
+
if count != len(target_group_ids):
|
152
|
+
raise HTTPException(
|
153
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
154
|
+
detail=f"Some UserGroups in {target_group_ids} do not exist.",
|
155
|
+
)
|
156
|
+
|
157
|
+
# Check that default group is not being removed
|
158
|
+
default_group_id = await _get_default_usergroup_id(db=db)
|
159
|
+
if default_group_id not in target_group_ids:
|
160
|
+
raise HTTPException(
|
161
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
162
|
+
detail=(
|
163
|
+
f"Cannot remove user from "
|
164
|
+
f"'{FRACTAL_DEFAULT_GROUP_NAME}' group.",
|
165
|
+
),
|
166
|
+
)
|
167
|
+
|
168
|
+
# Prepare lists of links to be removed
|
169
|
+
res = await db.execute(
|
170
|
+
select(LinkUserGroup)
|
171
|
+
.where(LinkUserGroup.user_id == user_id)
|
172
|
+
.where(LinkUserGroup.group_id.not_in(target_group_ids))
|
173
|
+
)
|
174
|
+
links_to_remove = res.scalars().all()
|
175
|
+
|
176
|
+
# Prepare lists of links to be added
|
177
|
+
res = await db.execute(
|
178
|
+
select(LinkUserGroup.group_id)
|
179
|
+
.where(LinkUserGroup.user_id == user_id)
|
180
|
+
.where(LinkUserGroup.group_id.in_(target_group_ids))
|
181
|
+
)
|
182
|
+
ids_links_already_in = res.scalars().all()
|
183
|
+
ids_links_to_add = set(target_group_ids) - set(ids_links_already_in)
|
184
|
+
|
185
|
+
# Remove/create links as needed
|
186
|
+
for link in links_to_remove:
|
187
|
+
logger.info(
|
188
|
+
f"Removing LinkUserGroup with {link.user_id=} "
|
189
|
+
f"and {link.group_id=}."
|
190
|
+
)
|
191
|
+
await db.delete(link)
|
192
|
+
for group_id in ids_links_to_add:
|
193
|
+
logger.info(
|
194
|
+
f"Creating new LinkUserGroup with {user_id=} " f"and {group_id=}."
|
195
|
+
)
|
196
|
+
db.add(LinkUserGroup(user_id=user_id, group_id=group_id))
|
197
|
+
await db.commit()
|
198
|
+
|
199
|
+
user_with_groups = await _get_single_user_with_groups(user, db)
|
200
|
+
|
201
|
+
return user_with_groups
|
202
|
+
|
203
|
+
|
206
204
|
@router_users.get(
|
207
205
|
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
208
206
|
)
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
3
3
|
from fastapi_users import schemas
|
4
4
|
from pydantic import BaseModel
|
5
5
|
from pydantic import Extra
|
6
|
+
from pydantic import Field
|
6
7
|
from pydantic import validator
|
7
8
|
|
8
9
|
from ._validators import val_unique_list
|
@@ -11,8 +12,8 @@ from ._validators import valstr
|
|
11
12
|
__all__ = (
|
12
13
|
"UserRead",
|
13
14
|
"UserUpdate",
|
15
|
+
"UserUpdateGroups",
|
14
16
|
"UserCreate",
|
15
|
-
"UserUpdateWithNewGroupIds",
|
16
17
|
)
|
17
18
|
|
18
19
|
|
@@ -45,7 +46,7 @@ class UserRead(schemas.BaseUser[int]):
|
|
45
46
|
oauth_accounts: list[OAuthAccountRead]
|
46
47
|
|
47
48
|
|
48
|
-
class UserUpdate(schemas.BaseUserUpdate):
|
49
|
+
class UserUpdate(schemas.BaseUserUpdate, extra=Extra.forbid):
|
49
50
|
"""
|
50
51
|
Schema for `User` update.
|
51
52
|
|
@@ -82,14 +83,6 @@ class UserUpdateStrict(BaseModel, extra=Extra.forbid):
|
|
82
83
|
pass
|
83
84
|
|
84
85
|
|
85
|
-
class UserUpdateWithNewGroupIds(UserUpdate):
|
86
|
-
new_group_ids: Optional[list[int]] = None
|
87
|
-
|
88
|
-
_val_unique = validator("new_group_ids", allow_reuse=True)(
|
89
|
-
val_unique_list("new_group_ids")
|
90
|
-
)
|
91
|
-
|
92
|
-
|
93
86
|
class UserCreate(schemas.BaseUserCreate):
|
94
87
|
"""
|
95
88
|
Schema for `User` creation.
|
@@ -103,3 +96,16 @@ class UserCreate(schemas.BaseUserCreate):
|
|
103
96
|
# Validators
|
104
97
|
|
105
98
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
99
|
+
|
100
|
+
|
101
|
+
class UserUpdateGroups(BaseModel, extra=Extra.forbid):
|
102
|
+
"""
|
103
|
+
Schema for `POST /auth/users/{user_id}/set-groups/`
|
104
|
+
|
105
|
+
"""
|
106
|
+
|
107
|
+
group_ids: list[int] = Field(min_items=1)
|
108
|
+
|
109
|
+
_group_ids = validator("group_ids", allow_reuse=True)(
|
110
|
+
val_unique_list("group_ids")
|
111
|
+
)
|
@@ -59,21 +59,10 @@ class UserGroupCreate(BaseModel, extra=Extra.forbid):
|
|
59
59
|
class UserGroupUpdate(BaseModel, extra=Extra.forbid):
|
60
60
|
"""
|
61
61
|
Schema for `UserGroup` update
|
62
|
-
|
63
|
-
NOTE: `new_user_ids` does not correspond to a column of the `UserGroup`
|
64
|
-
table, but it is rather used to create new `LinkUserGroup` rows.
|
65
|
-
|
66
|
-
Attributes:
|
67
|
-
new_user_ids: IDs of groups to be associated to user.
|
68
62
|
"""
|
69
63
|
|
70
|
-
new_user_ids: list[int] = Field(default_factory=list)
|
71
64
|
viewer_paths: Optional[list[str]] = None
|
72
65
|
|
73
|
-
_val_unique = validator("new_user_ids", allow_reuse=True)(
|
74
|
-
val_unique_list("new_user_ids")
|
75
|
-
)
|
76
|
-
|
77
66
|
@validator("viewer_paths")
|
78
67
|
def viewer_paths_validator(cls, value):
|
79
68
|
for i, path in enumerate(value):
|
@@ -1,4 +1,4 @@
|
|
1
|
-
fractal_server/__init__.py,sha256=
|
1
|
+
fractal_server/__init__.py,sha256=awxUTr6llNkjH1-5c-_yED0mpNs4APblXMX5R7fA0Qs,25
|
2
2
|
fractal_server/__main__.py,sha256=dEkCfzLLQrIlxsGC-HBfoR-RBMWnJDgNrxYTyzmE9c0,6146
|
3
3
|
fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
|
4
4
|
fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -29,7 +29,7 @@ fractal_server/app/routes/admin/v1.py,sha256=ggJZMeKhRijfVe2h2VzfIcpR15FqkKImANh
|
|
29
29
|
fractal_server/app/routes/admin/v2/__init__.py,sha256=KYrw0COmmMuIMp7c6YcYRXah4tEYplCWeROnPK1VTeg,681
|
30
30
|
fractal_server/app/routes/admin/v2/job.py,sha256=cbkFIRIIXaWmNsUFI7RAu8HpQ0mWn_bgoxtvWZxr-IA,7624
|
31
31
|
fractal_server/app/routes/admin/v2/project.py,sha256=luy-yiGX1JYTdPm1hpIdDUUqPm8xHuipLy9k2X6zu74,1223
|
32
|
-
fractal_server/app/routes/admin/v2/task.py,sha256=
|
32
|
+
fractal_server/app/routes/admin/v2/task.py,sha256=gShC2EAOYa0qTB69EXTDXz5Y375QoarOLv9T9vfntAE,4368
|
33
33
|
fractal_server/app/routes/admin/v2/task_group.py,sha256=DncrOAB4q-v3BAmxg35m4EohleriW_FLGE5gpW_Or08,8120
|
34
34
|
fractal_server/app/routes/admin/v2/task_group_lifecycle.py,sha256=0e0ZJ_k75TVHaT2o8Xk33DPDSgh-eBhZf-y4y7t-Adg,9429
|
35
35
|
fractal_server/app/routes/api/__init__.py,sha256=2IDheFi0OFdsUg7nbUiyahqybvpgXqeHUXIL2QtWrQQ,641
|
@@ -63,12 +63,12 @@ fractal_server/app/routes/api/v2/workflowtask.py,sha256=ciHTwXXFiFnMF7ZpJ3Xs0q6Y
|
|
63
63
|
fractal_server/app/routes/auth/__init__.py,sha256=fao6CS0WiAjHDTvBzgBVV_bSXFpEAeDBF6Z6q7rRkPc,1658
|
64
64
|
fractal_server/app/routes/auth/_aux_auth.py,sha256=ifkNocTYatBSMYGwiR14qohmvR9SfMldceiEj6uJBrU,4783
|
65
65
|
fractal_server/app/routes/auth/current_user.py,sha256=I3aVY5etWAJ_SH6t65Mj5TjvB2X8sAGuu1KG7FxLyPU,5883
|
66
|
-
fractal_server/app/routes/auth/group.py,sha256=
|
66
|
+
fractal_server/app/routes/auth/group.py,sha256=cS9I6pCIWGbOWc3gUBYmQq6yjFYzm6rVQDukWF_9L90,7721
|
67
67
|
fractal_server/app/routes/auth/login.py,sha256=tSu6OBLOieoBtMZB4JkBAdEgH2Y8KqPGSbwy7NIypIo,566
|
68
68
|
fractal_server/app/routes/auth/oauth.py,sha256=AnFHbjqL2AgBX3eksI931xD6RTtmbciHBEuGf9YJLjU,1895
|
69
69
|
fractal_server/app/routes/auth/register.py,sha256=DlHq79iOvGd_gt2v9uwtsqIKeO6i_GKaW59VIkllPqY,587
|
70
70
|
fractal_server/app/routes/auth/router.py,sha256=tzJrygXFZlmV_uWelVqTOJMEH-3Fr7ydwlgx1LxRjxY,527
|
71
|
-
fractal_server/app/routes/auth/users.py,sha256=
|
71
|
+
fractal_server/app/routes/auth/users.py,sha256=kZv-Ls224WBFiuvVeM584LhYq_BLz6HQ9HpWbWQxRRM,7808
|
72
72
|
fractal_server/app/routes/aux/__init__.py,sha256=LR4bR7RunHAK6jc9IR2bReQd-BdXADdnDccXI4uGeGY,731
|
73
73
|
fractal_server/app/routes/aux/_job.py,sha256=q-RCiW17yXnZKAC_0La52RLvhqhxuvbgQJ2MlGXOj8A,702
|
74
74
|
fractal_server/app/routes/aux/_runner.py,sha256=FdCVla5DxGAZ__aB7Z8dEJzD_RIeh5tftjrPyqkr8N8,895
|
@@ -135,8 +135,8 @@ fractal_server/app/runner/v2/task_interface.py,sha256=hT3p-bRGsLNAR_dNv_PYFoqzIF
|
|
135
135
|
fractal_server/app/runner/versions.py,sha256=dSaPRWqmFPHjg20kTCHmi_dmGNcCETflDtDLronNanU,852
|
136
136
|
fractal_server/app/schemas/__init__.py,sha256=stURAU_t3AOBaH0HSUbV-GKhlPKngnnIMoqWc3orFyI,135
|
137
137
|
fractal_server/app/schemas/_validators.py,sha256=T5EswIJAJRvawfzqWtPcN2INAfiBXyE4m0iwQm4ht-0,3149
|
138
|
-
fractal_server/app/schemas/user.py,sha256=
|
139
|
-
fractal_server/app/schemas/user_group.py,sha256=
|
138
|
+
fractal_server/app/schemas/user.py,sha256=icjox9gK_invW44Nh_L4CvqfRa92qghyQhmevyg09nQ,2243
|
139
|
+
fractal_server/app/schemas/user_group.py,sha256=t30Kd07PY43G_AqFDb8vjdInTeLeU9WvFZDx8fVLPSI,1750
|
140
140
|
fractal_server/app/schemas/user_settings.py,sha256=TalISeEfCrtN8LgqbLx1Q8ZPoeiZnbksg5NYAVzkIqY,3527
|
141
141
|
fractal_server/app/schemas/v1/__init__.py,sha256=CrBGgBhoemCvmZ70ZUchM-jfVAICnoa7AjZBAtL2UB0,1852
|
142
142
|
fractal_server/app/schemas/v1/applyworkflow.py,sha256=dYArxQAOBdUIEXX_Ejz8b9fBhEYu1nMm6b_Z6_P6TgA,4052
|
@@ -238,8 +238,8 @@ fractal_server/tasks/v2/utils_templates.py,sha256=C5WLuY3uGG2s53OEL-__H35-fmSlgu
|
|
238
238
|
fractal_server/urls.py,sha256=5o_qq7PzKKbwq12NHSQZDmDitn5RAOeQ4xufu-2v9Zk,448
|
239
239
|
fractal_server/utils.py,sha256=utvmBx8K9I8hRWFquxna2pBaOqe0JifDL_NVPmihEJI,3525
|
240
240
|
fractal_server/zip_tools.py,sha256=GjDgo_sf6V_DDg6wWeBlZu5zypIxycn_l257p_YVKGc,4876
|
241
|
-
fractal_server-2.9.
|
242
|
-
fractal_server-2.9.
|
243
|
-
fractal_server-2.9.
|
244
|
-
fractal_server-2.9.
|
245
|
-
fractal_server-2.9.
|
241
|
+
fractal_server-2.9.0a11.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
|
242
|
+
fractal_server-2.9.0a11.dist-info/METADATA,sha256=0SUdcGO7gPL9_VuNTLt10VitKNIgZTgQ5VL4FV2xjX8,4586
|
243
|
+
fractal_server-2.9.0a11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
244
|
+
fractal_server-2.9.0a11.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
|
245
|
+
fractal_server-2.9.0a11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|