fractal-server 2.4.0a0__py3-none-any.whl → 2.4.0a1__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.4.0a0"
1
+ __VERSION__ = "2.4.0a1"
@@ -7,6 +7,7 @@ from fastapi import HTTPException
7
7
  from fastapi import status
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
  from sqlmodel import col
10
+ from sqlmodel import func
10
11
  from sqlmodel import select
11
12
 
12
13
  from . import current_active_superuser
@@ -19,7 +20,6 @@ from fractal_server.app.models import LinkUserGroup
19
20
  from fractal_server.app.models import UserGroup
20
21
  from fractal_server.app.models import UserOAuth
21
22
 
22
-
23
23
  router_group = APIRouter()
24
24
 
25
25
 
@@ -31,9 +31,6 @@ async def get_list_user_groups(
31
31
  user: UserOAuth = Depends(current_active_superuser),
32
32
  db: AsyncSession = Depends(get_async_db),
33
33
  ) -> list[UserGroupRead]:
34
- """
35
- FIXME docstring
36
- """
37
34
 
38
35
  # Get all groups
39
36
  stm_all_groups = select(UserGroup)
@@ -46,7 +43,8 @@ async def get_list_user_groups(
46
43
  res = await db.execute(stm_all_links)
47
44
  links = res.scalars().all()
48
45
 
49
- # FIXME GROUPS: this must be optimized
46
+ # TODO: possible optimizations for this construction are listed in
47
+ # https://github.com/fractal-analytics-platform/fractal-server/issues/1742
50
48
  for ind, group in enumerate(groups):
51
49
  groups[ind] = dict(
52
50
  group.model_dump(),
@@ -68,9 +66,6 @@ async def get_single_user_group(
68
66
  user: UserOAuth = Depends(current_active_superuser),
69
67
  db: AsyncSession = Depends(get_async_db),
70
68
  ) -> UserGroupRead:
71
- """
72
- FIXME docstring
73
- """
74
69
  group = await _get_single_group_with_user_ids(group_id=group_id, db=db)
75
70
  return group
76
71
 
@@ -85,9 +80,6 @@ async def create_single_group(
85
80
  user: UserOAuth = Depends(current_active_superuser),
86
81
  db: AsyncSession = Depends(get_async_db),
87
82
  ) -> UserGroupRead:
88
- """
89
- FIXME docstring
90
- """
91
83
 
92
84
  # Check that name is not already in use
93
85
  existing_name_str = select(UserGroup).where(
@@ -119,24 +111,21 @@ async def update_single_group(
119
111
  user: UserOAuth = Depends(current_active_superuser),
120
112
  db: AsyncSession = Depends(get_async_db),
121
113
  ) -> UserGroupRead:
122
- """
123
- FIXME docstring
124
- """
125
114
 
126
115
  # Check that all required users exist
127
116
  # Note: The reason for introducing `col` is as in
128
117
  # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
129
- stm = select(UserOAuth).where(
118
+ stm = select(func.count()).where(
130
119
  col(UserOAuth.id).in_(group_update.new_user_ids)
131
120
  )
132
121
  res = await db.execute(stm)
133
- matching_users = res.scalars().unique().all()
134
- if not len(matching_users) == len(group_update.new_user_ids):
122
+ number_matching_users = res.scalar()
123
+ if number_matching_users != len(group_update.new_user_ids):
135
124
  raise HTTPException(
136
125
  status_code=status.HTTP_404_NOT_FOUND,
137
126
  detail=(
138
- f"At least user with IDs {group_update.new_user_ids} "
139
- "does not exist."
127
+ f"Not all requested users (IDs {group_update.new_user_ids}) "
128
+ "exist."
140
129
  ),
141
130
  )
142
131
 
@@ -161,9 +150,6 @@ async def delete_single_group(
161
150
  user: UserOAuth = Depends(current_active_superuser),
162
151
  db: AsyncSession = Depends(get_async_db),
163
152
  ) -> UserGroupRead:
164
- """
165
- FIXME docstring
166
- """
167
153
  raise HTTPException(
168
154
  status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
169
155
  detail=(
@@ -9,14 +9,18 @@ from fastapi_users import exceptions
9
9
  from fastapi_users import schemas
10
10
  from fastapi_users.router.common import ErrorCode
11
11
  from sqlalchemy.ext.asyncio import AsyncSession
12
+ from sqlmodel import col
13
+ from sqlmodel import func
12
14
  from sqlmodel import select
13
15
 
14
16
  from . import current_active_superuser
15
17
  from ...db import get_async_db
16
18
  from ...schemas.user import UserRead
17
19
  from ...schemas.user import UserUpdate
20
+ from ...schemas.user import UserUpdateWithNewGroupIds
18
21
  from ._aux_auth import _get_single_user_with_group_ids
19
22
  from fractal_server.app.models import LinkUserGroup
23
+ from fractal_server.app.models import UserGroup
20
24
  from fractal_server.app.models import UserOAuth
21
25
  from fractal_server.app.routes.auth._aux_auth import _user_or_404
22
26
  from fractal_server.app.security import get_user_manager
@@ -43,31 +47,96 @@ async def get_user(
43
47
  @router_users.patch("/users/{user_id}/", response_model=UserRead)
44
48
  async def patch_user(
45
49
  user_id: int,
46
- user_update: UserUpdate,
50
+ user_update: UserUpdateWithNewGroupIds,
47
51
  current_superuser: UserOAuth = Depends(current_active_superuser),
48
52
  user_manager: UserManager = Depends(get_user_manager),
49
53
  db: AsyncSession = Depends(get_async_db),
50
54
  ):
51
55
  """
52
56
  Custom version of the PATCH-user route from `fastapi-users`.
57
+
58
+ In order to keep the fastapi-users logic in place (which is convenient to
59
+ update user attributes), we split the endpoint into two branches. We either
60
+ go through the fastapi-users-based attribute-update branch, or through the
61
+ branch where we establish new user/group relationships.
62
+
63
+ Note that we prevent making both changes at the same time, since it would
64
+ be more complex to guarantee that endpoint error would leave the database
65
+ in the same state as before the API call.
53
66
  """
54
67
 
68
+ # We prevent simultaneous editing of both user attributes and user/group
69
+ # associations
70
+ user_update_dict_without_groups = user_update.dict(
71
+ exclude_unset=True, exclude={"new_group_ids"}
72
+ )
73
+ edit_attributes = user_update_dict_without_groups != {}
74
+ edit_groups = user_update.new_group_ids is not None
75
+ if edit_attributes and edit_groups:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
78
+ detail=(
79
+ "Cannot modify both user attributes and group membership. "
80
+ "Please make two independent PATCH calls"
81
+ ),
82
+ )
83
+
84
+ # Check that user exists
55
85
  user_to_patch = await _user_or_404(user_id, db)
56
86
 
57
- try:
58
- user = await user_manager.update(
59
- user_update, user_to_patch, safe=False, request=None
60
- )
61
- patched_user = schemas.model_validate(UserOAuth, user)
62
- except exceptions.InvalidPasswordException as e:
63
- raise HTTPException(
64
- status_code=status.HTTP_400_BAD_REQUEST,
65
- detail={
66
- "code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
67
- "reason": e.reason,
68
- },
87
+ if edit_groups:
88
+ # Establish new user/group relationships
89
+
90
+ # Check that all required groups exist
91
+ # Note: The reason for introducing `col` is as in
92
+ # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
93
+ stm = select(func.count()).where(
94
+ col(UserGroup.id).in_(user_update.new_group_ids)
69
95
  )
96
+ res = await db.execute(stm)
97
+ number_matching_groups = res.scalar()
98
+ if number_matching_groups != len(user_update.new_group_ids):
99
+ raise HTTPException(
100
+ status_code=status.HTTP_404_NOT_FOUND,
101
+ detail=(
102
+ "Not all requested groups (IDs: "
103
+ f"{user_update.new_group_ids}) exist."
104
+ ),
105
+ )
106
+
107
+ for new_group_id in user_update.new_group_ids:
108
+ link = LinkUserGroup(user_id=user_id, group_id=new_group_id)
109
+ db.add(link)
110
+ await db.commit()
111
+
112
+ patched_user = user_to_patch
113
+
114
+ elif edit_attributes:
115
+ # Modify user attributes
116
+ try:
117
+ user_update_without_groups = UserUpdate(
118
+ **user_update_dict_without_groups
119
+ )
120
+ user = await user_manager.update(
121
+ user_update_without_groups,
122
+ user_to_patch,
123
+ safe=False,
124
+ request=None,
125
+ )
126
+ patched_user = schemas.model_validate(UserOAuth, user)
127
+ except exceptions.InvalidPasswordException as e:
128
+ raise HTTPException(
129
+ status_code=status.HTTP_400_BAD_REQUEST,
130
+ detail={
131
+ "code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
132
+ "reason": e.reason,
133
+ },
134
+ )
135
+ else:
136
+ # Nothing to do, just continue
137
+ patched_user = user_to_patch
70
138
 
139
+ # Enrich user object with `group_ids` attribute
71
140
  patched_user_with_group_ids = await _get_single_user_with_group_ids(
72
141
  patched_user, db
73
142
  )
@@ -92,7 +161,8 @@ async def list_users(
92
161
  res = await db.execute(stm_all_links)
93
162
  links = res.scalars().all()
94
163
 
95
- # FIXME GROUPS: this must be optimized
164
+ # TODO: possible optimizations for this construction are listed in
165
+ # https://github.com/fractal-analytics-platform/fractal-server/issues/1742
96
166
  for ind, user in enumerate(user_list):
97
167
  user_list[ind] = dict(
98
168
  user.model_dump(),
@@ -16,6 +16,7 @@ __all__ = (
16
16
  "UserRead",
17
17
  "UserUpdate",
18
18
  "UserCreate",
19
+ "UserUpdateWithNewGroupIds",
19
20
  )
20
21
 
21
22
 
@@ -102,6 +103,10 @@ class UserUpdateStrict(BaseModel, extra=Extra.forbid):
102
103
  )
103
104
 
104
105
 
106
+ class UserUpdateWithNewGroupIds(UserUpdate):
107
+ new_group_ids: Optional[list[int]] = None
108
+
109
+
105
110
  class UserCreate(schemas.BaseUserCreate):
106
111
  """
107
112
  Schema for `User` creation.
@@ -43,7 +43,6 @@ from fastapi_users.exceptions import UserAlreadyExists
43
43
  from fastapi_users.models import ID
44
44
  from fastapi_users.models import OAP
45
45
  from fastapi_users.models import UP
46
- from sqlalchemy.exc import IntegrityError
47
46
  from sqlalchemy.ext.asyncio import AsyncSession
48
47
  from sqlalchemy.orm import selectinload
49
48
  from sqlmodel import func
@@ -295,12 +294,6 @@ async def _create_first_user(
295
294
  user = await user_manager.create(UserCreate(**kwargs))
296
295
  logger.info(f"User {user.email} created")
297
296
 
298
- except IntegrityError:
299
- logger.warning(
300
- f"Creation of user {email} failed with IntegrityError "
301
- "(likely due to concurrent attempts from different workers)."
302
- )
303
-
304
297
  except UserAlreadyExists:
305
298
  logger.warning(f"User {email} already exists")
306
299
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fractal-server
3
- Version: 2.4.0a0
3
+ Version: 2.4.0a1
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=3YbyR1dRXTwVjPcWPQWD72VIhMYsj-Cqwfmnwjo80JM,24
1
+ fractal_server/__init__.py,sha256=82UdKyiu21w5Ad9EyPoIbpiHLVC1oA6id-sFK4o1cnc,24
2
2
  fractal_server/__main__.py,sha256=I9hF_SYc-GTZWDZZhihwyUBK7BMU5GAecbPLTjkpW4U,5830
3
3
  fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
4
4
  fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -53,13 +53,13 @@ fractal_server/app/routes/api/v2/workflowtask.py,sha256=HoHFnVRDa0Cw1oqTea1Of6A5
53
53
  fractal_server/app/routes/auth/__init__.py,sha256=fao6CS0WiAjHDTvBzgBVV_bSXFpEAeDBF6Z6q7rRkPc,1658
54
54
  fractal_server/app/routes/auth/_aux_auth.py,sha256=Kpgiw5q1eiCYLFkfhTT7XJGBu1d08YM71CEHhNtfJ5g,3126
55
55
  fractal_server/app/routes/auth/current_user.py,sha256=LK0Z13NgaXYQ3FaQ3MNec0p2RRiKxKN31XIt2g9mcGk,2003
56
- fractal_server/app/routes/auth/group.py,sha256=ZVXeUisf0gUzHXHMINbaPXwXnzXtbcX01AuWXBvNdrA,4871
56
+ fractal_server/app/routes/auth/group.py,sha256=e0rVJQlocJ5RB8AEfQ5sufF4lJBmCFrGWQHvHqlpbIU,4817
57
57
  fractal_server/app/routes/auth/group_names.py,sha256=zvYDfhxKlDmbSr-oLXYy6WUVkPPTvzH6ZJtuoNdGZbE,960
58
58
  fractal_server/app/routes/auth/login.py,sha256=tSu6OBLOieoBtMZB4JkBAdEgH2Y8KqPGSbwy7NIypIo,566
59
59
  fractal_server/app/routes/auth/oauth.py,sha256=AnFHbjqL2AgBX3eksI931xD6RTtmbciHBEuGf9YJLjU,1895
60
60
  fractal_server/app/routes/auth/register.py,sha256=DlHq79iOvGd_gt2v9uwtsqIKeO6i_GKaW59VIkllPqY,587
61
61
  fractal_server/app/routes/auth/router.py,sha256=zWoZWiO69U48QFQf5tLRYQDWu8PUCj7GacnaFeW1n_I,618
62
- fractal_server/app/routes/auth/users.py,sha256=G9lFJ5zk-lAXDT1oDXXS66B_AxIlfnG35Sg7U44BdU4,3171
62
+ fractal_server/app/routes/auth/users.py,sha256=bUhoAyEgcJL5FNSn7pYYycAbmzMUiWl7NfmKaYG7mxs,6048
63
63
  fractal_server/app/routes/aux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  fractal_server/app/routes/aux/_job.py,sha256=q-RCiW17yXnZKAC_0La52RLvhqhxuvbgQJ2MlGXOj8A,702
65
65
  fractal_server/app/routes/aux/_runner.py,sha256=FdCVla5DxGAZ__aB7Z8dEJzD_RIeh5tftjrPyqkr8N8,895
@@ -126,7 +126,7 @@ fractal_server/app/runner/v2/v1_compat.py,sha256=t0ficzAHUFaaeI56nqTb4YEKxfARF7L
126
126
  fractal_server/app/runner/versions.py,sha256=dSaPRWqmFPHjg20kTCHmi_dmGNcCETflDtDLronNanU,852
127
127
  fractal_server/app/schemas/__init__.py,sha256=jiIf54owztXupv3PO6Ilh0qcrkh2RUzKq4bcEFqEfc4,40
128
128
  fractal_server/app/schemas/_validators.py,sha256=1dTOYr1IZykrxuQSV2-zuEMZbKe_nGwrfS7iUrsh-sE,3461
129
- fractal_server/app/schemas/user.py,sha256=ImNoZH7uqDkFuajVd0h9cDzJQ7D9YwYkluNuWtpk5_o,3199
129
+ fractal_server/app/schemas/user.py,sha256=__WVnUyQiWJ64JzlzyKowe2w2sBlF4S95RqdaUdA21c,3325
130
130
  fractal_server/app/schemas/user_group.py,sha256=CgW38Ett-DuRvN4tFEjG1jfX1csCFJIhVu5mjVlGEyI,1262
131
131
  fractal_server/app/schemas/v1/__init__.py,sha256=CrBGgBhoemCvmZ70ZUchM-jfVAICnoa7AjZBAtL2UB0,1852
132
132
  fractal_server/app/schemas/v1/applyworkflow.py,sha256=uuIh7fHlHEL4yLqL-dePI6-nfCsqgBYATmht7w_KITw,4302
@@ -149,7 +149,7 @@ fractal_server/app/schemas/v2/task.py,sha256=7IfxiZkaVqlARy7WYE_H8m7j_IEcuQaZORU
149
149
  fractal_server/app/schemas/v2/task_collection.py,sha256=8PG1bOqkfQqORMN0brWf6mHDmijt0bBW-mZsF7cSxUs,6129
150
150
  fractal_server/app/schemas/v2/workflow.py,sha256=Zzx3e-qgkH8le0FUmAx9UrV5PWd7bj14PPXUh_zgZXM,1827
151
151
  fractal_server/app/schemas/v2/workflowtask.py,sha256=atVuVN4aXsVEOmSd-vyg-8_8OnPmqx-gT75rXcn_AlQ,6552
152
- fractal_server/app/security/__init__.py,sha256=a-UEm1bdjgxLt88wmMyurf3X64UwxVw57yM9I8I2JYg,11687
152
+ fractal_server/app/security/__init__.py,sha256=FBxdrMvn2s3Gdmp1orqOpYji87JojLBzr9TMfblj1SI,11441
153
153
  fractal_server/config.py,sha256=R0VezSe2PEDjQjHEX2V29A1jMdoomdyECBjWNY15v_0,25049
154
154
  fractal_server/data_migrations/2_4_0.py,sha256=T1HRRWp9ZuXeVfBY6NRGxQ8aNIHVSftOMnB-CMrfvi8,2117
155
155
  fractal_server/data_migrations/README.md,sha256=_3AEFvDg9YkybDqCLlFPdDmGJvr6Tw7HRI14aZ3LOIw,398
@@ -207,8 +207,8 @@ fractal_server/tasks/v2/utils.py,sha256=JOyCacb6MNvrwfLNTyLwcz8y79J29YuJeJ2MK5kq
207
207
  fractal_server/urls.py,sha256=5o_qq7PzKKbwq12NHSQZDmDitn5RAOeQ4xufu-2v9Zk,448
208
208
  fractal_server/utils.py,sha256=b7WwFdcFZ8unyT65mloFToYuEDXpQoHRcmRNqrhd_dQ,2115
209
209
  fractal_server/zip_tools.py,sha256=xYpzBshysD2nmxkD5WLYqMzPYUcCRM3kYy-7n9bJL-U,4426
210
- fractal_server-2.4.0a0.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
211
- fractal_server-2.4.0a0.dist-info/METADATA,sha256=sZdr0mUMZvRpXhgArOMw16lfa282uBrl90dY_7QG4WU,4630
212
- fractal_server-2.4.0a0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
213
- fractal_server-2.4.0a0.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
214
- fractal_server-2.4.0a0.dist-info/RECORD,,
210
+ fractal_server-2.4.0a1.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
211
+ fractal_server-2.4.0a1.dist-info/METADATA,sha256=npvr_yl8n-KwghMVxD6kUx3yHf2wB_xTnFUFrtf7w7Q,4630
212
+ fractal_server-2.4.0a1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
213
+ fractal_server-2.4.0a1.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
214
+ fractal_server-2.4.0a1.dist-info/RECORD,,