fractal-server 2.17.2__py3-none-any.whl → 2.18.0__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 (108) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +2 -1
  3. fractal_server/app/models/linkuserproject.py +40 -0
  4. fractal_server/app/models/security.py +7 -5
  5. fractal_server/app/models/v2/job.py +13 -2
  6. fractal_server/app/models/v2/resource.py +13 -0
  7. fractal_server/app/routes/admin/v2/__init__.py +11 -11
  8. fractal_server/app/routes/admin/v2/accounting.py +2 -2
  9. fractal_server/app/routes/admin/v2/job.py +34 -23
  10. fractal_server/app/routes/admin/v2/sharing.py +103 -0
  11. fractal_server/app/routes/admin/v2/task.py +9 -8
  12. fractal_server/app/routes/admin/v2/task_group.py +94 -16
  13. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +20 -20
  14. fractal_server/app/routes/api/__init__.py +0 -9
  15. fractal_server/app/routes/api/v2/__init__.py +47 -47
  16. fractal_server/app/routes/api/v2/_aux_functions.py +65 -64
  17. fractal_server/app/routes/api/v2/_aux_functions_history.py +8 -3
  18. fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
  19. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +4 -4
  20. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +2 -2
  21. fractal_server/app/routes/api/v2/dataset.py +89 -77
  22. fractal_server/app/routes/api/v2/history.py +28 -16
  23. fractal_server/app/routes/api/v2/images.py +22 -8
  24. fractal_server/app/routes/api/v2/job.py +40 -24
  25. fractal_server/app/routes/api/v2/pre_submission_checks.py +13 -6
  26. fractal_server/app/routes/api/v2/project.py +48 -25
  27. fractal_server/app/routes/api/v2/sharing.py +311 -0
  28. fractal_server/app/routes/api/v2/status_legacy.py +22 -33
  29. fractal_server/app/routes/api/v2/submit.py +76 -71
  30. fractal_server/app/routes/api/v2/task.py +15 -17
  31. fractal_server/app/routes/api/v2/task_collection.py +18 -18
  32. fractal_server/app/routes/api/v2/task_collection_custom.py +11 -13
  33. fractal_server/app/routes/api/v2/task_collection_pixi.py +9 -9
  34. fractal_server/app/routes/api/v2/task_group.py +18 -18
  35. fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -26
  36. fractal_server/app/routes/api/v2/task_version_update.py +12 -9
  37. fractal_server/app/routes/api/v2/workflow.py +41 -29
  38. fractal_server/app/routes/api/v2/workflow_import.py +25 -23
  39. fractal_server/app/routes/api/v2/workflowtask.py +25 -17
  40. fractal_server/app/routes/auth/_aux_auth.py +100 -0
  41. fractal_server/app/routes/auth/current_user.py +0 -63
  42. fractal_server/app/routes/auth/group.py +1 -30
  43. fractal_server/app/routes/auth/router.py +2 -0
  44. fractal_server/app/routes/auth/users.py +9 -0
  45. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  46. fractal_server/app/schemas/user.py +29 -12
  47. fractal_server/app/schemas/user_group.py +0 -15
  48. fractal_server/app/schemas/v2/__init__.py +55 -48
  49. fractal_server/app/schemas/v2/dataset.py +35 -13
  50. fractal_server/app/schemas/v2/dumps.py +9 -9
  51. fractal_server/app/schemas/v2/job.py +11 -11
  52. fractal_server/app/schemas/v2/project.py +3 -3
  53. fractal_server/app/schemas/v2/resource.py +13 -4
  54. fractal_server/app/schemas/v2/sharing.py +99 -0
  55. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  56. fractal_server/app/schemas/v2/task.py +6 -6
  57. fractal_server/app/schemas/v2/task_collection.py +4 -4
  58. fractal_server/app/schemas/v2/task_group.py +16 -16
  59. fractal_server/app/schemas/v2/workflow.py +16 -16
  60. fractal_server/app/schemas/v2/workflowtask.py +14 -14
  61. fractal_server/app/security/__init__.py +1 -1
  62. fractal_server/app/shutdown.py +6 -6
  63. fractal_server/config/__init__.py +0 -6
  64. fractal_server/config/_data.py +0 -79
  65. fractal_server/config/_main.py +6 -1
  66. fractal_server/data_migrations/2_18_0.py +30 -0
  67. fractal_server/images/models.py +1 -2
  68. fractal_server/main.py +72 -11
  69. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  70. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  71. fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
  72. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  73. fractal_server/runner/config/_slurm.py +2 -0
  74. fractal_server/runner/executors/slurm_common/_batching.py +4 -10
  75. fractal_server/runner/executors/slurm_common/slurm_config.py +1 -0
  76. fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
  77. fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
  78. fractal_server/runner/v2/_local.py +4 -3
  79. fractal_server/runner/v2/_slurm_ssh.py +4 -3
  80. fractal_server/runner/v2/_slurm_sudo.py +4 -3
  81. fractal_server/runner/v2/runner.py +36 -17
  82. fractal_server/runner/v2/runner_functions.py +11 -14
  83. fractal_server/runner/v2/submit_workflow.py +22 -9
  84. fractal_server/tasks/v2/local/_utils.py +2 -2
  85. fractal_server/tasks/v2/local/collect.py +5 -6
  86. fractal_server/tasks/v2/local/collect_pixi.py +5 -6
  87. fractal_server/tasks/v2/local/deactivate.py +7 -7
  88. fractal_server/tasks/v2/local/deactivate_pixi.py +3 -3
  89. fractal_server/tasks/v2/local/delete.py +5 -5
  90. fractal_server/tasks/v2/local/reactivate.py +5 -5
  91. fractal_server/tasks/v2/local/reactivate_pixi.py +5 -5
  92. fractal_server/tasks/v2/ssh/collect.py +5 -5
  93. fractal_server/tasks/v2/ssh/collect_pixi.py +5 -5
  94. fractal_server/tasks/v2/ssh/deactivate.py +7 -7
  95. fractal_server/tasks/v2/ssh/deactivate_pixi.py +2 -2
  96. fractal_server/tasks/v2/ssh/delete.py +5 -5
  97. fractal_server/tasks/v2/ssh/reactivate.py +5 -5
  98. fractal_server/tasks/v2/ssh/reactivate_pixi.py +5 -5
  99. fractal_server/tasks/v2/utils_background.py +7 -7
  100. fractal_server/tasks/v2/utils_database.py +5 -5
  101. fractal_server/types/__init__.py +22 -0
  102. fractal_server/types/validators/__init__.py +3 -0
  103. fractal_server/types/validators/_common_validators.py +32 -0
  104. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
  105. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/RECORD +108 -98
  106. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
  107. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
  108. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.17.2"
1
+ __VERSION__ = "2.18.0"
@@ -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,
@@ -1,5 +1,11 @@
1
+ from sqlmodel import BOOLEAN
2
+ from sqlmodel import CheckConstraint
3
+ from sqlmodel import Column
1
4
  from sqlmodel import Field
5
+ from sqlmodel import Index
2
6
  from sqlmodel import SQLModel
7
+ from sqlmodel import String
8
+ from sqlmodel import column
3
9
 
4
10
 
5
11
  class LinkUserProjectV2(SQLModel, table=True):
@@ -11,3 +17,37 @@ class LinkUserProjectV2(SQLModel, table=True):
11
17
  foreign_key="projectv2.id", primary_key=True, ondelete="CASCADE"
12
18
  )
13
19
  user_id: int = Field(foreign_key="user_oauth.id", primary_key=True)
20
+
21
+ # TODO-2.18.1 drop server_default
22
+ is_owner: bool = Field(
23
+ sa_column=Column(BOOLEAN, server_default="true", nullable=False)
24
+ )
25
+ # TODO-2.18.1 drop server_default
26
+ is_verified: bool = Field(
27
+ sa_column=Column(BOOLEAN, server_default="true", nullable=False)
28
+ )
29
+ # TODO-2.18.1 drop server_default
30
+ permissions: str = Field(
31
+ sa_column=Column(String, server_default="'rwx'", nullable=False)
32
+ )
33
+
34
+ __table_args__ = (
35
+ Index(
36
+ "ix_linkuserprojectv2_one_owner_per_project",
37
+ "project_id",
38
+ unique=True,
39
+ postgresql_where=column("is_owner").is_(True),
40
+ ),
41
+ CheckConstraint(
42
+ "NOT (is_owner AND NOT is_verified)",
43
+ name="owner_is_verified",
44
+ ),
45
+ CheckConstraint(
46
+ "NOT (is_owner AND permissions <> 'rwx')",
47
+ name="owner_full_permissions",
48
+ ),
49
+ CheckConstraint(
50
+ "permissions IN ('r', 'rw', 'rwx')",
51
+ name="valid_permissions",
52
+ ),
53
+ )
@@ -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
- )
@@ -6,9 +6,11 @@ from sqlalchemy import Column
6
6
  from sqlalchemy.dialects.postgresql import JSONB
7
7
  from sqlalchemy.types import DateTime
8
8
  from sqlmodel import Field
9
+ from sqlmodel import Index
9
10
  from sqlmodel import SQLModel
11
+ from sqlmodel import text
10
12
 
11
- from fractal_server.app.schemas.v2 import JobStatusTypeV2
13
+ from fractal_server.app.schemas.v2 import JobStatusType
12
14
  from fractal_server.utils import get_timestamp
13
15
 
14
16
 
@@ -56,7 +58,7 @@ class JobV2(SQLModel, table=True):
56
58
  end_timestamp: datetime | None = Field(
57
59
  default=None, sa_column=Column(DateTime(timezone=True))
58
60
  )
59
- status: str = JobStatusTypeV2.SUBMITTED
61
+ status: str = JobStatusType.SUBMITTED
60
62
  log: str | None = None
61
63
  executor_error_log: str | None = None
62
64
 
@@ -66,3 +68,12 @@ class JobV2(SQLModel, table=True):
66
68
  type_filters: dict[str, bool] = Field(
67
69
  sa_column=Column(JSONB, nullable=False, server_default="{}")
68
70
  )
71
+
72
+ __table_args__ = (
73
+ Index(
74
+ "ix_jobv2_one_submitted_job_per_dataset",
75
+ "dataset_id",
76
+ unique=True,
77
+ postgresql_where=text(f"status = '{JobStatusType.SUBMITTED}'"),
78
+ ),
79
+ )
@@ -5,6 +5,7 @@ from typing import Self
5
5
  from sqlalchemy import Column
6
6
  from sqlalchemy.dialects.postgresql import JSONB
7
7
  from sqlalchemy.types import DateTime
8
+ from sqlmodel import BOOLEAN
8
9
  from sqlmodel import CheckConstraint
9
10
  from sqlmodel import Field
10
11
  from sqlmodel import SQLModel
@@ -43,6 +44,18 @@ class Resource(SQLModel, table=True):
43
44
  Address for ssh connections, when `type="slurm_ssh"`.
44
45
  """
45
46
 
47
+ prevent_new_submissions: bool = Field(
48
+ sa_column=Column(
49
+ BOOLEAN,
50
+ server_default="false",
51
+ nullable=False,
52
+ ),
53
+ )
54
+ """
55
+ When set to true: Prevent new job submissions and stop execution of
56
+ ongoing jobs as soon as the current task is complete.
57
+ """
58
+
46
59
  jobs_local_dir: str
47
60
  """
48
61
  Base local folder for job subfolders (containing artifacts and logs).
@@ -9,19 +9,19 @@ from .impersonate import router as impersonate_router
9
9
  from .job import router as job_router
10
10
  from .profile import router as profile_router
11
11
  from .resource import router as resource_router
12
+ from .sharing import router as sharing_router
12
13
  from .task import router as task_router
13
14
  from .task_group import router as task_group_router
14
15
  from .task_group_lifecycle import router as task_group_lifecycle_router
15
16
 
16
- router_admin_v2 = APIRouter()
17
+ router_admin = APIRouter()
17
18
 
18
- router_admin_v2.include_router(accounting_router, prefix="/accounting")
19
- router_admin_v2.include_router(job_router, prefix="/job")
20
- router_admin_v2.include_router(task_router, prefix="/task")
21
- router_admin_v2.include_router(task_group_router, prefix="/task-group")
22
- router_admin_v2.include_router(
23
- task_group_lifecycle_router, prefix="/task-group"
24
- )
25
- router_admin_v2.include_router(impersonate_router, prefix="/impersonate")
26
- router_admin_v2.include_router(resource_router, prefix="/resource")
27
- router_admin_v2.include_router(profile_router, prefix="/profile")
19
+ router_admin.include_router(accounting_router, prefix="/accounting")
20
+ router_admin.include_router(job_router, prefix="/job")
21
+ router_admin.include_router(task_router, prefix="/task")
22
+ router_admin.include_router(task_group_router, prefix="/task-group")
23
+ router_admin.include_router(task_group_lifecycle_router, prefix="/task-group")
24
+ router_admin.include_router(impersonate_router, prefix="/impersonate")
25
+ router_admin.include_router(resource_router, prefix="/resource")
26
+ router_admin.include_router(profile_router, prefix="/profile")
27
+ router_admin.include_router(sharing_router, prefix="/linkuserproject")
@@ -67,11 +67,11 @@ async def query_accounting(
67
67
  res = await db.execute(stm)
68
68
  records = res.scalars().all()
69
69
 
70
- return PaginationResponse[AccountingRecordRead](
70
+ return dict(
71
71
  total_count=total_count,
72
72
  page_size=page_size,
73
73
  current_page=page,
74
- items=[record.model_dump() for record in records],
74
+ items=records,
75
75
  )
76
76
 
77
77
 
@@ -24,9 +24,9 @@ from fractal_server.app.routes.pagination import PaginationRequest
24
24
  from fractal_server.app.routes.pagination import PaginationResponse
25
25
  from fractal_server.app.routes.pagination import get_pagination_params
26
26
  from fractal_server.app.schemas.v2 import HistoryUnitStatus
27
- from fractal_server.app.schemas.v2 import JobReadV2
28
- from fractal_server.app.schemas.v2 import JobStatusTypeV2
29
- from fractal_server.app.schemas.v2 import JobUpdateV2
27
+ from fractal_server.app.schemas.v2 import JobRead
28
+ from fractal_server.app.schemas.v2 import JobStatusType
29
+ from fractal_server.app.schemas.v2 import JobUpdate
30
30
  from fractal_server.runner.filenames import WORKFLOW_LOG_FILENAME
31
31
  from fractal_server.utils import get_timestamp
32
32
  from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
@@ -34,14 +34,14 @@ from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
34
34
  router = APIRouter()
35
35
 
36
36
 
37
- @router.get("/", response_model=PaginationResponse[JobReadV2])
37
+ @router.get("/", response_model=PaginationResponse[JobRead])
38
38
  async def view_job(
39
39
  id: int | None = None,
40
40
  user_id: int | None = None,
41
41
  project_id: int | None = None,
42
42
  dataset_id: int | None = None,
43
43
  workflow_id: int | None = None,
44
- status: JobStatusTypeV2 | None = None,
44
+ status: JobStatusType | None = None,
45
45
  start_timestamp_min: AwareDatetime | None = None,
46
46
  start_timestamp_max: AwareDatetime | None = None,
47
47
  end_timestamp_min: AwareDatetime | None = None,
@@ -50,12 +50,13 @@ async def view_job(
50
50
  pagination: PaginationRequest = Depends(get_pagination_params),
51
51
  user: UserOAuth = Depends(current_superuser_act),
52
52
  db: AsyncSession = Depends(get_async_db),
53
- ) -> PaginationResponse[JobReadV2]:
53
+ ) -> PaginationResponse[JobRead]:
54
54
  """
55
55
  Query `JobV2` table.
56
56
 
57
57
  Args:
58
58
  id: If not `None`, select a given `applyworkflow.id`.
59
+ user_id:
59
60
  project_id: If not `None`, select a given `applyworkflow.project_id`.
60
61
  dataset_id: If not `None`, select a given
61
62
  `applyworkflow.input_dataset_id`.
@@ -84,12 +85,22 @@ async def view_job(
84
85
  stm = stm.where(JobV2.id == id)
85
86
  stm_count = stm_count.where(JobV2.id == id)
86
87
  if user_id is not None:
87
- stm = stm.join(
88
- LinkUserProjectV2, LinkUserProjectV2.project_id == JobV2.project_id
89
- ).where(LinkUserProjectV2.user_id == user_id)
90
- stm_count = stm_count.join(
91
- LinkUserProjectV2, LinkUserProjectV2.project_id == JobV2.project_id
92
- ).where(LinkUserProjectV2.user_id == user_id)
88
+ stm = (
89
+ stm.join(
90
+ LinkUserProjectV2,
91
+ LinkUserProjectV2.project_id == JobV2.project_id,
92
+ )
93
+ .where(LinkUserProjectV2.user_id == user_id)
94
+ .where(LinkUserProjectV2.is_owner.is_(True))
95
+ )
96
+ stm_count = (
97
+ stm_count.join(
98
+ LinkUserProjectV2,
99
+ LinkUserProjectV2.project_id == JobV2.project_id,
100
+ )
101
+ .where(LinkUserProjectV2.user_id == user_id)
102
+ .where(LinkUserProjectV2.is_owner.is_(True))
103
+ )
93
104
  if project_id is not None:
94
105
  stm = stm.where(JobV2.project_id == project_id)
95
106
  stm_count = stm_count.where(JobV2.project_id == project_id)
@@ -135,21 +146,21 @@ async def view_job(
135
146
  for job in job_list:
136
147
  setattr(job, "log", None)
137
148
 
138
- return PaginationResponse[JobReadV2](
149
+ return dict(
139
150
  total_count=total_count,
140
151
  page_size=page_size,
141
152
  current_page=page,
142
- items=[job.model_dump() for job in job_list],
153
+ items=job_list,
143
154
  )
144
155
 
145
156
 
146
- @router.get("/{job_id}/", response_model=JobReadV2)
157
+ @router.get("/{job_id}/", response_model=JobRead)
147
158
  async def view_single_job(
148
159
  job_id: int,
149
160
  show_tmp_logs: bool = False,
150
161
  user: UserOAuth = Depends(current_superuser_act),
151
162
  db: AsyncSession = Depends(get_async_db),
152
- ) -> JobReadV2:
163
+ ) -> JobRead:
153
164
  job = await db.get(JobV2, job_id)
154
165
  if not job:
155
166
  raise HTTPException(
@@ -158,7 +169,7 @@ async def view_single_job(
158
169
  )
159
170
  await db.close()
160
171
 
161
- if show_tmp_logs and (job.status == JobStatusTypeV2.SUBMITTED):
172
+ if show_tmp_logs and (job.status == JobStatusType.SUBMITTED):
162
173
  try:
163
174
  with open(f"{job.working_dir}/{WORKFLOW_LOG_FILENAME}") as f:
164
175
  job.log = f.read()
@@ -168,13 +179,13 @@ async def view_single_job(
168
179
  return job
169
180
 
170
181
 
171
- @router.patch("/{job_id}/", response_model=JobReadV2)
182
+ @router.patch("/{job_id}/", response_model=JobRead)
172
183
  async def update_job(
173
- job_update: JobUpdateV2,
184
+ job_update: JobUpdate,
174
185
  job_id: int,
175
186
  user: UserOAuth = Depends(current_superuser_act),
176
187
  db: AsyncSession = Depends(get_async_db),
177
- ) -> JobReadV2 | None:
188
+ ) -> JobRead | None:
178
189
  """
179
190
  Change the status of an existing job.
180
191
 
@@ -187,13 +198,13 @@ async def update_job(
187
198
  status_code=status.HTTP_404_NOT_FOUND,
188
199
  detail=f"Job {job_id} not found",
189
200
  )
190
- if job.status != JobStatusTypeV2.SUBMITTED:
201
+ if job.status != JobStatusType.SUBMITTED:
191
202
  raise HTTPException(
192
203
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
193
204
  detail=f"Job {job_id} has status {job.status=} != 'submitted'.",
194
205
  )
195
206
 
196
- if job_update.status != JobStatusTypeV2.FAILED:
207
+ if job_update.status != JobStatusType.FAILED:
197
208
  raise HTTPException(
198
209
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
199
210
  detail=f"Cannot set job status to {job_update.status}",
@@ -206,7 +217,7 @@ async def update_job(
206
217
  job,
207
218
  "log",
208
219
  f"{job.log or ''}\nThis job was manually marked as "
209
- f"'{JobStatusTypeV2.FAILED}' by an admin ({timestamp.isoformat()}).",
220
+ f"'{JobStatusType.FAILED}' by an admin ({timestamp.isoformat()}).",
210
221
  )
211
222
 
212
223
  res = await db.execute(
@@ -0,0 +1,103 @@
1
+ from fastapi import APIRouter
2
+ from fastapi import Depends
3
+ from sqlalchemy import func
4
+ from sqlmodel import select
5
+
6
+ from fractal_server.app.db import AsyncSession
7
+ from fractal_server.app.db import get_async_db
8
+ from fractal_server.app.models import LinkUserProjectV2
9
+ from fractal_server.app.models import UserOAuth
10
+ from fractal_server.app.models.v2 import ProjectV2
11
+ from fractal_server.app.routes.auth import current_superuser_act
12
+ from fractal_server.app.routes.pagination import PaginationRequest
13
+ from fractal_server.app.routes.pagination import PaginationResponse
14
+ from fractal_server.app.routes.pagination import get_pagination_params
15
+ from fractal_server.app.schemas.v2 import LinkUserProjectRead
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ @router.get("/", response_model=PaginationResponse[LinkUserProjectRead])
21
+ async def view_link_user_project(
22
+ # User info
23
+ user_id: int | None = None,
24
+ # Project info
25
+ project_id: int | None = None,
26
+ project_name: str | None = None,
27
+ # Permissions
28
+ is_owner: bool | None = None,
29
+ is_verified: bool | None = None,
30
+ # -----
31
+ pagination: PaginationRequest = Depends(get_pagination_params),
32
+ superuser: UserOAuth = Depends(current_superuser_act),
33
+ db: AsyncSession = Depends(get_async_db),
34
+ ) -> PaginationResponse[LinkUserProjectRead]:
35
+ page = pagination.page
36
+ page_size = pagination.page_size
37
+
38
+ stm = (
39
+ select(
40
+ LinkUserProjectV2,
41
+ UserOAuth.email,
42
+ ProjectV2.name,
43
+ )
44
+ .join(UserOAuth, UserOAuth.id == LinkUserProjectV2.user_id)
45
+ .join(ProjectV2, ProjectV2.id == LinkUserProjectV2.project_id)
46
+ .order_by(UserOAuth.email, ProjectV2.name)
47
+ )
48
+ stm_count = (
49
+ select(func.count())
50
+ .select_from(LinkUserProjectV2)
51
+ .join(UserOAuth, UserOAuth.id == LinkUserProjectV2.user_id)
52
+ .join(ProjectV2, ProjectV2.id == LinkUserProjectV2.project_id)
53
+ )
54
+
55
+ if project_id is not None:
56
+ stm = stm.where(LinkUserProjectV2.project_id == project_id)
57
+ stm_count = stm_count.where(LinkUserProjectV2.project_id == project_id)
58
+ if project_name is not None:
59
+ stm = stm.where(ProjectV2.name.icontains(project_name))
60
+ stm_count = stm_count.where(ProjectV2.name.icontains(project_name))
61
+ if user_id is not None:
62
+ stm = stm.where(LinkUserProjectV2.user_id == user_id)
63
+ stm_count = stm_count.where(LinkUserProjectV2.user_id == user_id)
64
+ if is_owner is not None:
65
+ stm = stm.where(LinkUserProjectV2.is_owner == is_owner)
66
+ stm_count = stm_count.where(LinkUserProjectV2.is_owner == is_owner)
67
+ if is_verified is not None:
68
+ stm = stm.where(LinkUserProjectV2.is_verified == is_verified)
69
+ stm_count = stm_count.where(
70
+ LinkUserProjectV2.is_verified == is_verified
71
+ )
72
+
73
+ res_total_count = await db.execute(stm_count)
74
+
75
+ total_count = res_total_count.scalar()
76
+ if page_size is None:
77
+ page_size = total_count
78
+ else:
79
+ stm = stm.offset((page - 1) * page_size).limit(page_size)
80
+
81
+ res = await db.execute(stm)
82
+ items = res.all()
83
+
84
+ return PaginationResponse[LinkUserProjectRead](
85
+ total_count=total_count,
86
+ page_size=page_size,
87
+ current_page=page,
88
+ items=[
89
+ dict(
90
+ # User info
91
+ user_id=linkuserproject.user_id,
92
+ user_email=user_email,
93
+ # Project info
94
+ project_id=linkuserproject.project_id,
95
+ project_name=project_name,
96
+ # Permissions
97
+ is_verified=linkuserproject.is_verified,
98
+ is_owner=linkuserproject.is_owner,
99
+ permissions=linkuserproject.permissions,
100
+ )
101
+ for linkuserproject, user_email, project_name in items
102
+ ],
103
+ )
@@ -23,7 +23,7 @@ from fractal_server.app.schemas.v2.task import TaskType
23
23
  router = APIRouter()
24
24
 
25
25
 
26
- class TaskV2Minimal(BaseModel):
26
+ class TaskMinimal(BaseModel):
27
27
  id: int
28
28
  name: str
29
29
  type: str
@@ -39,7 +39,7 @@ class ProjectUser(BaseModel):
39
39
  email: EmailStr
40
40
 
41
41
 
42
- class TaskV2Relationship(BaseModel):
42
+ class TaskRelationship(BaseModel):
43
43
  workflow_id: int
44
44
  workflow_name: str
45
45
  project_id: int
@@ -47,12 +47,12 @@ class TaskV2Relationship(BaseModel):
47
47
  project_users: list[ProjectUser] = Field(default_factory=list)
48
48
 
49
49
 
50
- class TaskV2Info(BaseModel):
51
- task: TaskV2Minimal
52
- relationships: list[TaskV2Relationship]
50
+ class TaskInfo(BaseModel):
51
+ task: TaskMinimal
52
+ relationships: list[TaskRelationship]
53
53
 
54
54
 
55
- @router.get("/", response_model=PaginationResponse[TaskV2Info])
55
+ @router.get("/", response_model=PaginationResponse[TaskInfo])
56
56
  async def query_tasks(
57
57
  id: int | None = None,
58
58
  source: str | None = None,
@@ -66,7 +66,7 @@ async def query_tasks(
66
66
  pagination: PaginationRequest = Depends(get_pagination_params),
67
67
  user: UserOAuth = Depends(current_superuser_act),
68
68
  db: AsyncSession = Depends(get_async_db),
69
- ) -> PaginationResponse[TaskV2Info]:
69
+ ) -> PaginationResponse[TaskInfo]:
70
70
  """
71
71
  Query `TaskV2` and get information about related workflows and projects.
72
72
  """
@@ -148,6 +148,7 @@ async def query_tasks(
148
148
  LinkUserProjectV2.user_id == UserOAuth.id,
149
149
  )
150
150
  .where(LinkUserProjectV2.project_id == project_id)
151
+ .where(LinkUserProjectV2.is_owner.is_(True))
151
152
  )
152
153
  project_users[project_id] = [
153
154
  ProjectUser(id=p_user[0], email=p_user[1])
@@ -169,7 +170,7 @@ async def query_tasks(
169
170
  ],
170
171
  )
171
172
  )
172
- return PaginationResponse[TaskV2Info](
173
+ return dict(
173
174
  total_count=total_count,
174
175
  page_size=page_size,
175
176
  current_page=page,