fractal-server 2.14.12__py3-none-any.whl → 2.14.13__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.14.12"
1
+ __VERSION__ = "2.14.13"
@@ -0,0 +1,163 @@
1
+ import itertools
2
+
3
+ from sqlmodel import select
4
+
5
+ from fractal_server.app.db import AsyncSession
6
+ from fractal_server.app.models import LinkUserGroup
7
+ from fractal_server.app.models.v2 import TaskGroupV2
8
+ from fractal_server.exceptions import UnreachableBranchError
9
+ from fractal_server.logger import set_logger
10
+
11
+
12
+ logger = set_logger(__name__)
13
+
14
+
15
+ async def _disambiguate_task_groups(
16
+ *,
17
+ matching_task_groups: list[TaskGroupV2],
18
+ user_id: int,
19
+ default_group_id: int,
20
+ db: AsyncSession,
21
+ ) -> TaskGroupV2 | None:
22
+ """
23
+ Find ownership-based top-priority task group, if any.
24
+
25
+ Args:
26
+ matching_task_groups:
27
+ user_id:
28
+ default_group_id:
29
+ db:
30
+
31
+ Returns:
32
+ The task group or `None`.
33
+ """
34
+
35
+ # Highest priority: task groups created by user
36
+ list_user_ids = [tg.user_id for tg in matching_task_groups]
37
+ try:
38
+ ind_user_id = list_user_ids.index(user_id)
39
+ task_group = matching_task_groups[ind_user_id]
40
+ logger.debug(
41
+ "[_disambiguate_task_groups] "
42
+ f"Found task group {task_group.id} with {user_id=}, return."
43
+ )
44
+ return task_group
45
+ except ValueError:
46
+ logger.debug(
47
+ "[_disambiguate_task_groups] "
48
+ f"No task group with {user_id=}, continue."
49
+ )
50
+
51
+ # Medium priority: task groups owned by default user group
52
+ list_user_group_ids = [tg.user_group_id for tg in matching_task_groups]
53
+ try:
54
+ ind_user_group_id = list_user_group_ids.index(default_group_id)
55
+ task_group = matching_task_groups[ind_user_group_id]
56
+ logger.debug(
57
+ "[_disambiguate_task_groups] "
58
+ f"Found task group {task_group.id} with {user_id=}, return."
59
+ )
60
+ return task_group
61
+ except ValueError:
62
+ logger.debug(
63
+ "[_disambiguate_task_groups] "
64
+ "No task group with user_group_id="
65
+ f"{default_group_id}, continue."
66
+ )
67
+
68
+ # Lowest priority: task groups owned by other groups, sorted
69
+ # according to age of the user/usergroup link
70
+ logger.debug(
71
+ "[_disambiguate_task_groups] "
72
+ "Sort remaining task groups by oldest-user-link."
73
+ )
74
+ stm = (
75
+ select(LinkUserGroup.group_id)
76
+ .where(LinkUserGroup.user_id == user_id)
77
+ .where(LinkUserGroup.group_id.in_(list_user_group_ids))
78
+ .order_by(LinkUserGroup.timestamp_created.asc())
79
+ )
80
+ res = await db.execute(stm)
81
+ oldest_user_group_id = res.scalars().first()
82
+ logger.debug(
83
+ "[_disambiguate_task_groups] " f"Result: {oldest_user_group_id=}."
84
+ )
85
+ task_group = next(
86
+ iter(
87
+ task_group
88
+ for task_group in matching_task_groups
89
+ if task_group.user_group_id == oldest_user_group_id
90
+ ),
91
+ None,
92
+ )
93
+ return task_group
94
+
95
+
96
+ async def _disambiguate_task_groups_not_none(
97
+ *,
98
+ matching_task_groups: list[TaskGroupV2],
99
+ user_id: int,
100
+ default_group_id: int,
101
+ db: AsyncSession,
102
+ ) -> TaskGroupV2:
103
+ """
104
+ Find ownership-based top-priority task group, and fail otherwise.
105
+
106
+ Args:
107
+ matching_task_groups:
108
+ user_id:
109
+ default_group_id:
110
+ db:
111
+
112
+ Returns:
113
+ The top-priority task group.
114
+ """
115
+ task_group = await _disambiguate_task_groups(
116
+ matching_task_groups=matching_task_groups,
117
+ user_id=user_id,
118
+ default_group_id=default_group_id,
119
+ db=db,
120
+ )
121
+ if task_group is None:
122
+ error_msg = (
123
+ "[_disambiguate_task_groups_not_none] Could not find a task "
124
+ f"group ({user_id=}, {default_group_id=})."
125
+ )
126
+ logger.error(f"UnreachableBranchError {error_msg}")
127
+ raise UnreachableBranchError(error_msg)
128
+ else:
129
+ return task_group
130
+
131
+
132
+ async def remove_duplicate_task_groups(
133
+ *,
134
+ task_groups: list[TaskGroupV2],
135
+ user_id: int,
136
+ default_group_id: int,
137
+ db: AsyncSession,
138
+ ) -> list[TaskGroupV2]:
139
+ """
140
+ Extract a single task group for each `version`.
141
+
142
+ Args:
143
+ task_groups: A list of task groups with identical `pkg_name`
144
+ user_id: User ID
145
+
146
+ Returns:
147
+ New list of task groups with no duplicate `(pkg_name,version)` entries
148
+ """
149
+
150
+ new_task_groups = [
151
+ (
152
+ await _disambiguate_task_groups_not_none(
153
+ matching_task_groups=list(groups),
154
+ user_id=user_id,
155
+ default_group_id=default_group_id,
156
+ db=db,
157
+ )
158
+ )
159
+ for version, groups in itertools.groupby(
160
+ task_groups, key=lambda tg: tg.version
161
+ )
162
+ ]
163
+ return new_task_groups
@@ -1,8 +1,13 @@
1
+ import itertools
2
+
1
3
  from fastapi import APIRouter
2
4
  from fastapi import Depends
3
5
  from fastapi import HTTPException
4
6
  from fastapi import Response
5
7
  from fastapi import status
8
+ from packaging.version import InvalidVersion
9
+ from packaging.version import parse
10
+ from packaging.version import Version
6
11
  from pydantic.types import AwareDatetime
7
12
  from sqlmodel import or_
8
13
  from sqlmodel import select
@@ -10,6 +15,7 @@ from sqlmodel import select
10
15
  from ._aux_functions_tasks import _get_task_group_full_access
11
16
  from ._aux_functions_tasks import _get_task_group_read_access
12
17
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
18
+ from ._aux_task_group_disambiguation import remove_duplicate_task_groups
13
19
  from fractal_server.app.db import AsyncSession
14
20
  from fractal_server.app.db import get_async_db
15
21
  from fractal_server.app.models import LinkUserGroup
@@ -18,6 +24,7 @@ from fractal_server.app.models.v2 import TaskGroupActivityV2
18
24
  from fractal_server.app.models.v2 import TaskGroupV2
19
25
  from fractal_server.app.models.v2 import WorkflowTaskV2
20
26
  from fractal_server.app.routes.auth import current_active_user
27
+ from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
21
28
  from fractal_server.app.routes.auth._aux_auth import (
22
29
  _verify_user_belongs_to_group,
23
30
  )
@@ -33,6 +40,26 @@ router = APIRouter()
33
40
  logger = set_logger(__name__)
34
41
 
35
42
 
43
+ def _version_sort_key(
44
+ task_group: TaskGroupV2,
45
+ ) -> tuple[int, Version | str | None]:
46
+ """
47
+ Returns a tuple used as (reverse) ordering key for TaskGroups in
48
+ `get_task_group_list`.
49
+ The TaskGroups with a parsable versions are the first in order,
50
+ sorted according to the sorting rules of packaging.version.Version.
51
+ Next in order we have the TaskGroups with non-null non-parsable versions,
52
+ sorted alphabetically.
53
+ Last we have the TaskGroups with null version.
54
+ """
55
+ if task_group.version is None:
56
+ return (0, task_group.version)
57
+ try:
58
+ return (2, parse(task_group.version))
59
+ except InvalidVersion:
60
+ return (1, task_group.version)
61
+
62
+
36
63
  @router.get("/activity/", response_model=list[TaskGroupActivityV2Read])
37
64
  async def get_task_group_activity_list(
38
65
  task_group_activity_id: int | None = None,
@@ -97,14 +124,14 @@ async def get_task_group_activity(
97
124
  return activity
98
125
 
99
126
 
100
- @router.get("/", response_model=list[TaskGroupReadV2])
127
+ @router.get("/", response_model=list[tuple[str, list[TaskGroupReadV2]]])
101
128
  async def get_task_group_list(
102
129
  user: UserOAuth = Depends(current_active_user),
103
130
  db: AsyncSession = Depends(get_async_db),
104
131
  only_active: bool = False,
105
132
  only_owner: bool = False,
106
133
  args_schema: bool = True,
107
- ) -> list[TaskGroupReadV2]:
134
+ ) -> list[tuple[str, list[TaskGroupReadV2]]]:
108
135
  """
109
136
  Get all accessible TaskGroups
110
137
  """
@@ -119,7 +146,7 @@ async def get_task_group_list(
119
146
  )
120
147
  ),
121
148
  )
122
- stm = select(TaskGroupV2).where(condition)
149
+ stm = select(TaskGroupV2).where(condition).order_by(TaskGroupV2.pkg_name)
123
150
  if only_active:
124
151
  stm = stm.where(TaskGroupV2.active)
125
152
 
@@ -132,7 +159,28 @@ async def get_task_group_list(
132
159
  setattr(task, "args_schema_non_parallel", None)
133
160
  setattr(task, "args_schema_parallel", None)
134
161
 
135
- return task_groups
162
+ default_group_id = await _get_default_usergroup_id(db)
163
+ grouped_result = [
164
+ (
165
+ pkg_name,
166
+ sorted(
167
+ (
168
+ await remove_duplicate_task_groups(
169
+ task_groups=list(groups),
170
+ user_id=user.id,
171
+ default_group_id=default_group_id,
172
+ db=db,
173
+ )
174
+ ),
175
+ key=_version_sort_key,
176
+ reverse=True,
177
+ ),
178
+ )
179
+ for pkg_name, groups in itertools.groupby(
180
+ task_groups, key=lambda tg: tg.pkg_name
181
+ )
182
+ ]
183
+ return grouped_result
136
184
 
137
185
 
138
186
  @router.get("/{task_group_id}/", response_model=TaskGroupReadV2)
@@ -21,6 +21,9 @@ from ._aux_functions_tasks import _check_type_filters_compatibility
21
21
  from fractal_server.app.models import LinkUserGroup
22
22
  from fractal_server.app.models import UserOAuth
23
23
  from fractal_server.app.models.v2 import TaskGroupV2
24
+ from fractal_server.app.routes.api.v2._aux_task_group_disambiguation import (
25
+ _disambiguate_task_groups,
26
+ )
24
27
  from fractal_server.app.routes.auth import current_active_user
25
28
  from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
26
29
  from fractal_server.app.schemas.v2 import TaskImportV2
@@ -85,76 +88,6 @@ async def _get_task_by_source(
85
88
  return task_id
86
89
 
87
90
 
88
- async def _disambiguate_task_groups(
89
- *,
90
- matching_task_groups: list[TaskGroupV2],
91
- user_id: int,
92
- db: AsyncSession,
93
- default_group_id: int,
94
- ) -> TaskV2 | None:
95
- """
96
- Disambiguate task groups based on ownership information.
97
- """
98
- # Highest priority: task groups created by user
99
- for task_group in matching_task_groups:
100
- if task_group.user_id == user_id:
101
- logger.info(
102
- "[_disambiguate_task_groups] "
103
- f"Found task group {task_group.id} with {user_id=}, return."
104
- )
105
- return task_group
106
- logger.info(
107
- "[_disambiguate_task_groups] "
108
- f"No task group found with {user_id=}, continue."
109
- )
110
-
111
- # Medium priority: task groups owned by default user group
112
- for task_group in matching_task_groups:
113
- if task_group.user_group_id == default_group_id:
114
- logger.info(
115
- "[_disambiguate_task_groups] "
116
- f"Found task group {task_group.id} with user_group_id="
117
- f"{default_group_id}, return."
118
- )
119
- return task_group
120
- logger.info(
121
- "[_disambiguate_task_groups] "
122
- "No task group found with user_group_id="
123
- f"{default_group_id}, continue."
124
- )
125
-
126
- # Lowest priority: task groups owned by other groups, sorted
127
- # according to age of the user/usergroup link
128
- logger.info(
129
- "[_disambiguate_task_groups] "
130
- "Now sorting remaining task groups by oldest-user-link."
131
- )
132
- user_group_ids = [
133
- task_group.user_group_id for task_group in matching_task_groups
134
- ]
135
- stm = (
136
- select(LinkUserGroup.group_id)
137
- .where(LinkUserGroup.user_id == user_id)
138
- .where(LinkUserGroup.group_id.in_(user_group_ids))
139
- .order_by(LinkUserGroup.timestamp_created.asc())
140
- )
141
- res = await db.execute(stm)
142
- oldest_user_group_id = res.scalars().first()
143
- logger.info(
144
- "[_disambiguate_task_groups] "
145
- f"Result of sorting: {oldest_user_group_id=}."
146
- )
147
- task_group = next(
148
- iter(
149
- task_group
150
- for task_group in matching_task_groups
151
- if task_group.user_group_id == oldest_user_group_id
152
- ),
153
- None,
154
- )
155
- return task_group
156
-
157
-
158
91
  async def _get_task_by_taskimport(
159
92
  *,
160
93
  task_import: TaskImportV2,
@@ -0,0 +1,2 @@
1
+ class UnreachableBranchError(RuntimeError):
2
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fractal-server
3
- Version: 2.14.12
3
+ Version: 2.14.13
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License: BSD-3-Clause
6
6
  Author: Tommaso Comparin
@@ -1,4 +1,4 @@
1
- fractal_server/__init__.py,sha256=2Uq7ENBsUHaOd4e_XjB355CgrpEtZcAw2o31N0wSuJE,24
1
+ fractal_server/__init__.py,sha256=6bdBE5bJoA01yAEh1heVK-Ta-FFnpeeh531ZgEO8YXY,24
2
2
  fractal_server/__main__.py,sha256=rkM8xjY1KeS3l63irB8yCrlVobR-73uDapC4wvrIlxI,6957
3
3
  fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
4
4
  fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -35,6 +35,7 @@ fractal_server/app/routes/api/v2/_aux_functions_history.py,sha256=Z23xwvBaVEEQ5B
35
35
  fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py,sha256=GpKfw9yj01LmOAuNMTOreU1PFkCKpjK5oCt7_wp35-A,6741
36
36
  fractal_server/app/routes/api/v2/_aux_functions_task_version_update.py,sha256=WLDOYCnb6fnS5avKflyx6yN24Vo1n5kJk5ZyiKbzb8Y,1175
37
37
  fractal_server/app/routes/api/v2/_aux_functions_tasks.py,sha256=MFYnyNPBACSHXTDLXe6cSennnpmlpajN84iivOOMW7Y,11599
38
+ fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py,sha256=2sK7-bZzcl3-2mkx62tw0MPxeUYVDch30DSWgdhouHI,4615
38
39
  fractal_server/app/routes/api/v2/dataset.py,sha256=6u4MFqJ3YZ0Zq6Xx8CRMrTPKW55ZaR63Uno21DqFr4Q,8889
39
40
  fractal_server/app/routes/api/v2/history.py,sha256=BEmf_ENF5HNMy8yXrxRdo4280rWuRUa1Jw4u8R9-LQQ,15477
40
41
  fractal_server/app/routes/api/v2/images.py,sha256=TS1ltUhP0_SaViupdHrSh3MLDi5OVk-lOhE1VCVyZj0,7869
@@ -46,11 +47,11 @@ fractal_server/app/routes/api/v2/submit.py,sha256=_BDkWtFdo8-p7kZ0Oxaidei04MfuBe
46
47
  fractal_server/app/routes/api/v2/task.py,sha256=cUFrCxFOLGlRV7UCbUMHs4Xy4tIc3pqwG8gEqVP5GcU,6939
47
48
  fractal_server/app/routes/api/v2/task_collection.py,sha256=FGMhTnU88Umd8nMdriUYPtpTtAHcRBRrZIYyOesFhrU,12577
48
49
  fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=EfGpv6W7xDyuYYp6E7XAcXLJiLNAImUHFqMDLgfh-4s,6730
49
- fractal_server/app/routes/api/v2/task_group.py,sha256=iShTvM9nJQhQLwR8ZpQRucVwYhJ7t00Lbesqh3M6mY4,7361
50
+ fractal_server/app/routes/api/v2/task_group.py,sha256=Xfsj5Wy0NOIkbeYsdqyFke4mkaeq0riJeTrGCHbt-eM,9059
50
51
  fractal_server/app/routes/api/v2/task_group_lifecycle.py,sha256=C2U2V76YbbqDWmErJ98MH9C2C26Lve2p_35FZ1dNmXg,9095
51
52
  fractal_server/app/routes/api/v2/task_version_update.py,sha256=h2c6aTLXj0_ZyBuHVsD5-ZTNMGEUpS96qZ4Ot1jlb74,7974
52
53
  fractal_server/app/routes/api/v2/workflow.py,sha256=gwMtpfUY_JiTv5_R_q1I9WNkp6nTqEVtYx8jWNJRxcU,10227
53
- fractal_server/app/routes/api/v2/workflow_import.py,sha256=Q4CnkSV47F11j6DkNT_U3AhwBK-LSsWWegItfdoOJ6c,11167
54
+ fractal_server/app/routes/api/v2/workflow_import.py,sha256=kOGDaCj0jCGK1WSYGbnUjtUg2U1YxUY9UMH-2ilqJg4,9027
54
55
  fractal_server/app/routes/api/v2/workflowtask.py,sha256=vVqEoJa3lrMl2CU94WoxFaqO3U0QImPgvrkkUNdqDOU,7462
55
56
  fractal_server/app/routes/auth/__init__.py,sha256=fao6CS0WiAjHDTvBzgBVV_bSXFpEAeDBF6Z6q7rRkPc,1658
56
57
  fractal_server/app/routes/auth/_aux_auth.py,sha256=UZgauY0V6mSqjte_sYI1cBl2h8bcbLaeWzgpl1jdJlk,4883
@@ -131,6 +132,7 @@ fractal_server/config.py,sha256=ldI9VzEWmwU75Z7zVku6I-rXGKS3bJDdCifZnwad9-4,2592
131
132
  fractal_server/data_migrations/2_14_10.py,sha256=gMRR5QB0SDv0ToEiXVLg1VrHprM_Ii-9O1Kg-ZF-YhY,1599
132
133
  fractal_server/data_migrations/README.md,sha256=_3AEFvDg9YkybDqCLlFPdDmGJvr6Tw7HRI14aZ3LOIw,398
133
134
  fractal_server/data_migrations/tools.py,sha256=LeMeASwYGtEqd-3wOLle6WARdTGAimoyMmRbbJl-hAM,572
135
+ fractal_server/exceptions.py,sha256=7ftpWwNsTQmNonWCynhH5ErUh1haPPhIaVPrNHla7-o,53
134
136
  fractal_server/gunicorn_fractal.py,sha256=u6U01TLGlXgq1v8QmEpLih3QnsInZD7CqphgJ_GrGzc,1230
135
137
  fractal_server/images/__init__.py,sha256=-_wjoKtSX02P1KjDxDP_EXKvmbONTRmbf7iGVTsyBpM,154
136
138
  fractal_server/images/models.py,sha256=6WchcIzLLLwdkLNRfg71Dl4Y-9UFLPyrrzh1lWgjuP0,1245
@@ -214,8 +216,8 @@ fractal_server/types/validators/_workflow_task_arguments_validators.py,sha256=HL
214
216
  fractal_server/urls.py,sha256=QjIKAC1a46bCdiPMu3AlpgFbcv6a4l3ABcd5xz190Og,471
215
217
  fractal_server/utils.py,sha256=FCY6HUsRnnbsWkT2kwQ2izijiHuCrCD3Kh50G0QudxE,3531
216
218
  fractal_server/zip_tools.py,sha256=tqz_8f-vQ9OBRW-4OQfO6xxY-YInHTyHmZxU7U4PqZo,4885
217
- fractal_server-2.14.12.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
218
- fractal_server-2.14.12.dist-info/METADATA,sha256=c2Sxzo4hw2jBpCe9rBhWVz4x0UCztabOGI9YBZqu7vU,4244
219
- fractal_server-2.14.12.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
220
- fractal_server-2.14.12.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
221
- fractal_server-2.14.12.dist-info/RECORD,,
219
+ fractal_server-2.14.13.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
220
+ fractal_server-2.14.13.dist-info/METADATA,sha256=-h8oCvhvIAs4Sp0G5zxavNQkpS6NkBsM8XHHmCT47EE,4244
221
+ fractal_server-2.14.13.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
222
+ fractal_server-2.14.13.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
223
+ fractal_server-2.14.13.dist-info/RECORD,,