fractal-server 2.9.0a10__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.
@@ -1 +1 @@
1
- __VERSION__ = "2.9.0a10"
1
+ __VERSION__ = "2.9.0a11"
@@ -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: UserUpdateWithNewGroupIds,
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
- if edit_groups:
96
- # Establish new user/group relationships
97
-
98
- # Check that all required groups exist
99
- # Note: The reason for introducing `col` is as in
100
- # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
101
- stm = select(func.count()).where(
102
- col(UserGroup.id).in_(user_update.new_group_ids)
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fractal-server
3
- Version: 2.9.0a10
3
+ Version: 2.9.0a11
4
4
  Summary: Server component of the Fractal analytics platform
5
5
  Home-page: https://github.com/fractal-analytics-platform/fractal-server
6
6
  License: BSD-3-Clause
@@ -1,4 +1,4 @@
1
- fractal_server/__init__.py,sha256=bOb7y0ERfwPyi1oCYCLL0TUUUEB3QNEldOxpWFB5zGo,25
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
@@ -68,7 +68,7 @@ fractal_server/app/routes/auth/login.py,sha256=tSu6OBLOieoBtMZB4JkBAdEgH2Y8KqPGS
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=FzKNoB-wD32AkVOj1Vi29lGGyOl8NSMCRL9tEhxqpJk,8403
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,7 +135,7 @@ 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=aUD8YAcfYTEO06TEUoTx4heVrXFiX7E2Mb8D2--4FsA,2130
138
+ fractal_server/app/schemas/user.py,sha256=icjox9gK_invW44Nh_L4CvqfRa92qghyQhmevyg09nQ,2243
139
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
@@ -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.0a10.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
242
- fractal_server-2.9.0a10.dist-info/METADATA,sha256=m2WwGM-wSD_5ffYqaGCVERw4RZ7yhM7VtxSdlGt3xGQ,4586
243
- fractal_server-2.9.0a10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
244
- fractal_server-2.9.0a10.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
245
- fractal_server-2.9.0a10.dist-info/RECORD,,
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,,