fractal-server 2.8.1__py3-none-any.whl → 2.9.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 (81) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -35
  3. fractal_server/app/models/v2/__init__.py +3 -3
  4. fractal_server/app/models/v2/task.py +0 -72
  5. fractal_server/app/models/v2/task_group.py +113 -0
  6. fractal_server/app/routes/admin/v1.py +13 -30
  7. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  8. fractal_server/app/routes/admin/v2/job.py +13 -24
  9. fractal_server/app/routes/admin/v2/task.py +13 -0
  10. fractal_server/app/routes/admin/v2/task_group.py +75 -14
  11. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
  12. fractal_server/app/routes/api/v1/project.py +7 -19
  13. fractal_server/app/routes/api/v2/__init__.py +11 -2
  14. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
  16. fractal_server/app/routes/api/v2/submit.py +19 -24
  17. fractal_server/app/routes/api/v2/task_collection.py +33 -65
  18. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  19. fractal_server/app/routes/api/v2/task_group.py +86 -14
  20. fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
  21. fractal_server/app/routes/api/v2/workflow.py +1 -1
  22. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  23. fractal_server/app/routes/auth/current_user.py +60 -17
  24. fractal_server/app/routes/auth/group.py +67 -39
  25. fractal_server/app/routes/auth/users.py +97 -99
  26. fractal_server/app/routes/aux/__init__.py +20 -0
  27. fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
  28. fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
  29. fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
  30. fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
  31. fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
  32. fractal_server/app/schemas/_validators.py +0 -15
  33. fractal_server/app/schemas/user.py +16 -10
  34. fractal_server/app/schemas/user_group.py +0 -11
  35. fractal_server/app/schemas/v1/applyworkflow.py +0 -8
  36. fractal_server/app/schemas/v1/dataset.py +0 -5
  37. fractal_server/app/schemas/v1/project.py +0 -5
  38. fractal_server/app/schemas/v1/state.py +0 -5
  39. fractal_server/app/schemas/v1/workflow.py +0 -5
  40. fractal_server/app/schemas/v2/__init__.py +4 -2
  41. fractal_server/app/schemas/v2/dataset.py +0 -6
  42. fractal_server/app/schemas/v2/job.py +0 -8
  43. fractal_server/app/schemas/v2/project.py +0 -5
  44. fractal_server/app/schemas/v2/task_collection.py +0 -21
  45. fractal_server/app/schemas/v2/task_group.py +59 -8
  46. fractal_server/app/schemas/v2/workflow.py +0 -5
  47. fractal_server/app/security/__init__.py +17 -0
  48. fractal_server/config.py +61 -59
  49. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
  50. fractal_server/ssh/_fabric.py +156 -83
  51. fractal_server/tasks/utils.py +2 -12
  52. fractal_server/tasks/v2/local/__init__.py +3 -0
  53. fractal_server/tasks/v2/local/_utils.py +70 -0
  54. fractal_server/tasks/v2/local/collect.py +291 -0
  55. fractal_server/tasks/v2/local/deactivate.py +218 -0
  56. fractal_server/tasks/v2/local/reactivate.py +159 -0
  57. fractal_server/tasks/v2/ssh/__init__.py +3 -0
  58. fractal_server/tasks/v2/ssh/_utils.py +87 -0
  59. fractal_server/tasks/v2/ssh/collect.py +311 -0
  60. fractal_server/tasks/v2/ssh/deactivate.py +253 -0
  61. fractal_server/tasks/v2/ssh/reactivate.py +202 -0
  62. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  63. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  64. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  65. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  66. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  67. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  68. fractal_server/tasks/v2/utils_background.py +42 -127
  69. fractal_server/tasks/v2/utils_templates.py +32 -2
  70. fractal_server/utils.py +4 -2
  71. fractal_server/zip_tools.py +21 -4
  72. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
  73. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/RECORD +77 -64
  74. fractal_server/app/models/v2/collection_state.py +0 -22
  75. fractal_server/tasks/v2/collection_local.py +0 -357
  76. fractal_server/tasks/v2/collection_ssh.py +0 -352
  77. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  78. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  79. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
  80. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
  81. {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.8.1"
1
+ __VERSION__ = "2.9.0"
@@ -2,17 +2,14 @@
2
2
  `db` module, loosely adapted from
3
3
  https://testdriven.io/blog/fastapi-sqlmodel/#async-sqlmodel
4
4
  """
5
- import sqlite3
6
5
  from typing import AsyncGenerator
7
6
  from typing import Generator
8
7
 
9
8
  from sqlalchemy import create_engine
10
- from sqlalchemy import event
11
9
  from sqlalchemy.ext.asyncio import AsyncSession
12
10
  from sqlalchemy.ext.asyncio import create_async_engine
13
11
  from sqlalchemy.orm import Session as DBSyncSession
14
12
  from sqlalchemy.orm import sessionmaker
15
- from sqlalchemy.pool import StaticPool
16
13
 
17
14
  from ...config import get_settings
18
15
  from ...logger import set_logger
@@ -21,14 +18,6 @@ from ...syringe import Inject
21
18
 
22
19
  logger = set_logger(__name__)
23
20
 
24
- SQLITE_WARNING_MESSAGE = (
25
- "SQLite is supported (supported version >=3.37, "
26
- f"current {sqlite3.sqlite_version=}) "
27
- "but discouraged in production. "
28
- "Given its partial support for ForeignKey constraints, "
29
- "database consistency cannot be guaranteed."
30
- )
31
-
32
21
 
33
22
  class DB:
34
23
  """
@@ -56,14 +45,7 @@ class DB:
56
45
  settings = Inject(get_settings)
57
46
  settings.check_db()
58
47
 
59
- if settings.DB_ENGINE == "sqlite":
60
- logger.warning(SQLITE_WARNING_MESSAGE)
61
- # Set some sqlite-specific options
62
- engine_kwargs_async = dict(poolclass=StaticPool)
63
- else:
64
- engine_kwargs_async = {
65
- "pool_pre_ping": True,
66
- }
48
+ engine_kwargs_async = {"pool_pre_ping": True}
67
49
 
68
50
  cls._engine_async = create_async_engine(
69
51
  settings.DATABASE_ASYNC_URL,
@@ -83,15 +65,7 @@ class DB:
83
65
  settings = Inject(get_settings)
84
66
  settings.check_db()
85
67
 
86
- if settings.DB_ENGINE == "sqlite":
87
- logger.warning(SQLITE_WARNING_MESSAGE)
88
- # Set some sqlite-specific options
89
- engine_kwargs_sync = dict(
90
- poolclass=StaticPool,
91
- connect_args={"check_same_thread": False},
92
- )
93
- else:
94
- engine_kwargs_sync = {}
68
+ engine_kwargs_sync = {}
95
69
 
96
70
  cls._engine_sync = create_engine(
97
71
  settings.DATABASE_SYNC_URL,
@@ -107,13 +81,6 @@ class DB:
107
81
  future=True,
108
82
  )
109
83
 
110
- @event.listens_for(cls._engine_sync, "connect")
111
- def set_sqlite_pragma(dbapi_connection, connection_record):
112
- if settings.DB_ENGINE == "sqlite":
113
- cursor = dbapi_connection.cursor()
114
- cursor.execute("PRAGMA journal_mode=WAL")
115
- cursor.close()
116
-
117
84
  @classmethod
118
85
  async def get_async_db(cls) -> AsyncGenerator[AsyncSession, None]:
119
86
  """
@@ -2,12 +2,12 @@
2
2
  v2 `models` module
3
3
  """
4
4
  from ..linkuserproject import LinkUserProjectV2
5
- from .collection_state import CollectionStateV2
6
5
  from .dataset import DatasetV2
7
6
  from .job import JobV2
8
7
  from .project import ProjectV2
9
- from .task import TaskGroupV2
10
8
  from .task import TaskV2
9
+ from .task_group import TaskGroupActivityV2
10
+ from .task_group import TaskGroupV2
11
11
  from .workflow import WorkflowV2
12
12
  from .workflowtask import WorkflowTaskV2
13
13
 
@@ -16,8 +16,8 @@ __all__ = [
16
16
  "DatasetV2",
17
17
  "JobV2",
18
18
  "ProjectV2",
19
- "CollectionStateV2",
20
19
  "TaskGroupV2",
20
+ "TaskGroupActivityV2",
21
21
  "TaskV2",
22
22
  "WorkflowTaskV2",
23
23
  "WorkflowV2",
@@ -1,17 +1,12 @@
1
- from datetime import datetime
2
1
  from typing import Any
3
2
  from typing import Optional
4
3
 
5
4
  from pydantic import HttpUrl
6
5
  from sqlalchemy import Column
7
- from sqlalchemy.types import DateTime
8
6
  from sqlalchemy.types import JSON
9
7
  from sqlmodel import Field
10
- from sqlmodel import Relationship
11
8
  from sqlmodel import SQLModel
12
9
 
13
- from fractal_server.utils import get_timestamp
14
-
15
10
 
16
11
  class TaskV2(SQLModel, table=True):
17
12
  id: Optional[int] = Field(default=None, primary_key=True)
@@ -51,70 +46,3 @@ class TaskV2(SQLModel, table=True):
51
46
  tags: list[str] = Field(
52
47
  sa_column=Column(JSON, server_default="[]", nullable=False)
53
48
  )
54
-
55
-
56
- class TaskGroupV2(SQLModel, table=True):
57
- id: Optional[int] = Field(default=None, primary_key=True)
58
- task_list: list[TaskV2] = Relationship(
59
- sa_relationship_kwargs=dict(
60
- lazy="selectin", cascade="all, delete-orphan"
61
- ),
62
- )
63
-
64
- user_id: int = Field(foreign_key="user_oauth.id")
65
- user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
66
-
67
- origin: str
68
- pkg_name: str
69
- version: Optional[str] = None
70
- python_version: Optional[str] = None
71
- path: Optional[str] = None
72
- venv_path: Optional[str] = None
73
- wheel_path: Optional[str] = None
74
- pip_extras: Optional[str] = None
75
- pinned_package_versions: dict[str, str] = Field(
76
- sa_column=Column(
77
- JSON,
78
- server_default="{}",
79
- default={},
80
- nullable=True,
81
- ),
82
- )
83
-
84
- active: bool = True
85
- timestamp_created: datetime = Field(
86
- default_factory=get_timestamp,
87
- sa_column=Column(DateTime(timezone=True), nullable=False),
88
- )
89
-
90
- @property
91
- def pip_install_string(self) -> str:
92
- """
93
- Prepare string to be used in `python -m pip install`.
94
- """
95
- extras = f"[{self.pip_extras}]" if self.pip_extras is not None else ""
96
-
97
- if self.wheel_path is not None:
98
- return f"{self.wheel_path}{extras}"
99
- else:
100
- if self.version is None:
101
- raise ValueError(
102
- "Cannot run `pip_install_string` with "
103
- f"{self.pkg_name=}, {self.wheel_path=}, {self.version=}."
104
- )
105
- return f"{self.pkg_name}{extras}=={self.version}"
106
-
107
- @property
108
- def pinned_package_versions_string(self) -> str:
109
- """
110
- Prepare string to be used in `python -m pip install`.
111
- """
112
- if self.pinned_package_versions is None:
113
- return ""
114
- output = " ".join(
115
- [
116
- f"{key}=={value}"
117
- for key, value in self.pinned_package_versions.items()
118
- ]
119
- )
120
- return output
@@ -0,0 +1,113 @@
1
+ from datetime import datetime
2
+ from datetime import timezone
3
+ from typing import Optional
4
+
5
+ from sqlalchemy import Column
6
+ from sqlalchemy.types import DateTime
7
+ from sqlalchemy.types import JSON
8
+ from sqlmodel import Field
9
+ from sqlmodel import Relationship
10
+ from sqlmodel import SQLModel
11
+
12
+ from .task import TaskV2
13
+ from fractal_server.utils import get_timestamp
14
+
15
+
16
+ class TaskGroupV2(SQLModel, table=True):
17
+ id: Optional[int] = Field(default=None, primary_key=True)
18
+ task_list: list[TaskV2] = Relationship(
19
+ sa_relationship_kwargs=dict(
20
+ lazy="selectin", cascade="all, delete-orphan"
21
+ ),
22
+ )
23
+
24
+ user_id: int = Field(foreign_key="user_oauth.id")
25
+ user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
26
+
27
+ origin: str
28
+ pkg_name: str
29
+ version: Optional[str] = None
30
+ python_version: Optional[str] = None
31
+ path: Optional[str] = None
32
+ wheel_path: Optional[str] = None
33
+ pip_extras: Optional[str] = None
34
+ pinned_package_versions: dict[str, str] = Field(
35
+ sa_column=Column(
36
+ JSON,
37
+ server_default="{}",
38
+ default={},
39
+ nullable=True,
40
+ ),
41
+ )
42
+ pip_freeze: Optional[str] = None
43
+ venv_path: Optional[str] = None
44
+ venv_size_in_kB: Optional[int] = None
45
+ venv_file_number: Optional[int] = None
46
+
47
+ active: bool = True
48
+ timestamp_created: datetime = Field(
49
+ default_factory=get_timestamp,
50
+ sa_column=Column(DateTime(timezone=True), nullable=False),
51
+ )
52
+ timestamp_last_used: datetime = Field(
53
+ default_factory=get_timestamp,
54
+ sa_column=Column(
55
+ DateTime(timezone=True),
56
+ nullable=False,
57
+ server_default=(
58
+ datetime(2024, 11, 20, tzinfo=timezone.utc).isoformat()
59
+ ),
60
+ ),
61
+ )
62
+
63
+ @property
64
+ def pip_install_string(self) -> str:
65
+ """
66
+ Prepare string to be used in `python -m pip install`.
67
+ """
68
+ extras = f"[{self.pip_extras}]" if self.pip_extras is not None else ""
69
+
70
+ if self.wheel_path is not None:
71
+ return f"{self.wheel_path}{extras}"
72
+ else:
73
+ if self.version is None:
74
+ raise ValueError(
75
+ "Cannot run `pip_install_string` with "
76
+ f"{self.pkg_name=}, {self.wheel_path=}, {self.version=}."
77
+ )
78
+ return f"{self.pkg_name}{extras}=={self.version}"
79
+
80
+ @property
81
+ def pinned_package_versions_string(self) -> str:
82
+ """
83
+ Prepare string to be used in `python -m pip install`.
84
+ """
85
+ if self.pinned_package_versions is None:
86
+ return ""
87
+ output = " ".join(
88
+ [
89
+ f"{key}=={value}"
90
+ for key, value in self.pinned_package_versions.items()
91
+ ]
92
+ )
93
+ return output
94
+
95
+
96
+ class TaskGroupActivityV2(SQLModel, table=True):
97
+
98
+ id: Optional[int] = Field(default=None, primary_key=True)
99
+ user_id: int = Field(foreign_key="user_oauth.id")
100
+ taskgroupv2_id: Optional[int] = Field(foreign_key="taskgroupv2.id")
101
+ timestamp_started: datetime = Field(
102
+ default_factory=get_timestamp,
103
+ sa_column=Column(DateTime(timezone=True), nullable=False),
104
+ )
105
+ pkg_name: str
106
+ version: str
107
+ status: str
108
+ action: str
109
+ log: Optional[str] = None
110
+ timestamp_ended: Optional[datetime] = Field(
111
+ default=None,
112
+ sa_column=Column(DateTime(timezone=True)),
113
+ )
@@ -2,7 +2,6 @@
2
2
  Definition of `/admin` routes.
3
3
  """
4
4
  from datetime import datetime
5
- from datetime import timezone
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
@@ -15,8 +14,6 @@ from fastapi.responses import StreamingResponse
15
14
  from sqlalchemy import func
16
15
  from sqlmodel import select
17
16
 
18
- from ....config import get_settings
19
- from ....syringe import Inject
20
17
  from ....utils import get_timestamp
21
18
  from ....zip_tools import _zip_folder_to_byte_stream_iterator
22
19
  from ...db import AsyncSession
@@ -36,27 +33,11 @@ from ..aux._job import _write_shutdown_file
36
33
  from ..aux._runner import _check_shutdown_is_supported
37
34
  from fractal_server.app.models import UserOAuth
38
35
  from fractal_server.app.routes.auth import current_active_superuser
36
+ from fractal_server.app.routes.aux import _raise_if_naive_datetime
39
37
 
40
38
  router_admin_v1 = APIRouter()
41
39
 
42
40
 
43
- def _convert_to_db_timestamp(dt: datetime) -> datetime:
44
- """
45
- This function takes a timezone-aware datetime and converts it to UTC.
46
- If using SQLite, it also removes the timezone information in order to make
47
- the datetime comparable with datetimes in the database.
48
- """
49
- if dt.tzinfo is None:
50
- raise HTTPException(
51
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
52
- detail=f"The timestamp provided has no timezone information: {dt}",
53
- )
54
- _dt = dt.astimezone(timezone.utc)
55
- if Inject(get_settings).DB_ENGINE == "sqlite":
56
- return _dt.replace(tzinfo=None)
57
- return _dt
58
-
59
-
60
41
  @router_admin_v1.get("/project/", response_model=list[ProjectReadV1])
61
42
  async def view_project(
62
43
  id: Optional[int] = None,
@@ -73,6 +54,7 @@ async def view_project(
73
54
  id: If not `None`, select a given `project.id`.
74
55
  user_id: If not `None`, select a given `project.user_id`.
75
56
  """
57
+ _raise_if_naive_datetime(timestamp_created_min, timestamp_created_max)
76
58
 
77
59
  stm = select(Project)
78
60
 
@@ -82,10 +64,8 @@ async def view_project(
82
64
  if user_id is not None:
83
65
  stm = stm.where(Project.user_list.any(UserOAuth.id == user_id))
84
66
  if timestamp_created_min is not None:
85
- timestamp_created_min = _convert_to_db_timestamp(timestamp_created_min)
86
67
  stm = stm.where(Project.timestamp_created >= timestamp_created_min)
87
68
  if timestamp_created_max is not None:
88
- timestamp_created_max = _convert_to_db_timestamp(timestamp_created_max)
89
69
  stm = stm.where(Project.timestamp_created <= timestamp_created_max)
90
70
 
91
71
  res = await db.execute(stm)
@@ -115,6 +95,8 @@ async def view_workflow(
115
95
  name_contains: If not `None`, select workflows such that their
116
96
  `name` attribute contains `name_contains` (case-insensitive).
117
97
  """
98
+ _raise_if_naive_datetime(timestamp_created_min, timestamp_created_max)
99
+
118
100
  stm = select(Workflow)
119
101
 
120
102
  if user_id is not None:
@@ -131,10 +113,8 @@ async def view_workflow(
131
113
  func.lower(Workflow.name).contains(name_contains.lower())
132
114
  )
133
115
  if timestamp_created_min is not None:
134
- timestamp_created_min = _convert_to_db_timestamp(timestamp_created_min)
135
116
  stm = stm.where(Workflow.timestamp_created >= timestamp_created_min)
136
117
  if timestamp_created_max is not None:
137
- timestamp_created_max = _convert_to_db_timestamp(timestamp_created_max)
138
118
  stm = stm.where(Workflow.timestamp_created <= timestamp_created_max)
139
119
 
140
120
  res = await db.execute(stm)
@@ -166,6 +146,8 @@ async def view_dataset(
166
146
  `name` attribute contains `name_contains` (case-insensitive).
167
147
  type: If not `None`, select a given `dataset.type`.
168
148
  """
149
+ _raise_if_naive_datetime(timestamp_created_min, timestamp_created_max)
150
+
169
151
  stm = select(Dataset)
170
152
 
171
153
  if user_id is not None:
@@ -184,10 +166,8 @@ async def view_dataset(
184
166
  if type is not None:
185
167
  stm = stm.where(Dataset.type == type)
186
168
  if timestamp_created_min is not None:
187
- timestamp_created_min = _convert_to_db_timestamp(timestamp_created_min)
188
169
  stm = stm.where(Dataset.timestamp_created >= timestamp_created_min)
189
170
  if timestamp_created_max is not None:
190
- timestamp_created_max = _convert_to_db_timestamp(timestamp_created_max)
191
171
  stm = stm.where(Dataset.timestamp_created <= timestamp_created_max)
192
172
 
193
173
  res = await db.execute(stm)
@@ -237,6 +217,13 @@ async def view_job(
237
217
  log: If `True`, include `job.log`, if `False`
238
218
  `job.log` is set to `None`.
239
219
  """
220
+ _raise_if_naive_datetime(
221
+ start_timestamp_min,
222
+ start_timestamp_max,
223
+ end_timestamp_min,
224
+ end_timestamp_max,
225
+ )
226
+
240
227
  stm = select(ApplyWorkflow)
241
228
 
242
229
  if id is not None:
@@ -256,16 +243,12 @@ async def view_job(
256
243
  if status is not None:
257
244
  stm = stm.where(ApplyWorkflow.status == status)
258
245
  if start_timestamp_min is not None:
259
- start_timestamp_min = _convert_to_db_timestamp(start_timestamp_min)
260
246
  stm = stm.where(ApplyWorkflow.start_timestamp >= start_timestamp_min)
261
247
  if start_timestamp_max is not None:
262
- start_timestamp_max = _convert_to_db_timestamp(start_timestamp_max)
263
248
  stm = stm.where(ApplyWorkflow.start_timestamp <= start_timestamp_max)
264
249
  if end_timestamp_min is not None:
265
- end_timestamp_min = _convert_to_db_timestamp(end_timestamp_min)
266
250
  stm = stm.where(ApplyWorkflow.end_timestamp >= end_timestamp_min)
267
251
  if end_timestamp_max is not None:
268
- end_timestamp_max = _convert_to_db_timestamp(end_timestamp_max)
269
252
  stm = stm.where(ApplyWorkflow.end_timestamp <= end_timestamp_max)
270
253
 
271
254
  res = await db.execute(stm)
@@ -7,6 +7,7 @@ from .job import router as job_router
7
7
  from .project import router as project_router
8
8
  from .task import router as task_router
9
9
  from .task_group import router as task_group_router
10
+ from .task_group_lifecycle import router as task_group_lifecycle_router
10
11
 
11
12
  router_admin_v2 = APIRouter()
12
13
 
@@ -14,3 +15,6 @@ router_admin_v2.include_router(job_router, prefix="/job")
14
15
  router_admin_v2.include_router(project_router, prefix="/project")
15
16
  router_admin_v2.include_router(task_router, prefix="/task")
16
17
  router_admin_v2.include_router(task_group_router, prefix="/task-group")
18
+ router_admin_v2.include_router(
19
+ task_group_lifecycle_router, prefix="/task-group"
20
+ )
@@ -1,5 +1,4 @@
1
1
  from datetime import datetime
2
- from datetime import timezone
3
2
  from pathlib import Path
4
3
  from typing import Optional
5
4
 
@@ -17,37 +16,19 @@ from fractal_server.app.models import UserOAuth
17
16
  from fractal_server.app.models.v2 import JobV2
18
17
  from fractal_server.app.models.v2 import ProjectV2
19
18
  from fractal_server.app.routes.auth import current_active_superuser
19
+ from fractal_server.app.routes.aux import _raise_if_naive_datetime
20
20
  from fractal_server.app.routes.aux._job import _write_shutdown_file
21
21
  from fractal_server.app.routes.aux._runner import _check_shutdown_is_supported
22
22
  from fractal_server.app.runner.filenames import WORKFLOW_LOG_FILENAME
23
23
  from fractal_server.app.schemas.v2 import JobReadV2
24
24
  from fractal_server.app.schemas.v2 import JobStatusTypeV2
25
25
  from fractal_server.app.schemas.v2 import JobUpdateV2
26
- from fractal_server.config import get_settings
27
- from fractal_server.syringe import Inject
28
26
  from fractal_server.utils import get_timestamp
29
27
  from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
30
28
 
31
29
  router = APIRouter()
32
30
 
33
31
 
34
- def _convert_to_db_timestamp(dt: datetime) -> datetime:
35
- """
36
- This function takes a timezone-aware datetime and converts it to UTC.
37
- If using SQLite, it also removes the timezone information in order to make
38
- the datetime comparable with datetimes in the database.
39
- """
40
- if dt.tzinfo is None:
41
- raise HTTPException(
42
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
43
- detail=f"The timestamp provided has no timezone information: {dt}",
44
- )
45
- _dt = dt.astimezone(timezone.utc)
46
- if Inject(get_settings).DB_ENGINE == "sqlite":
47
- return _dt.replace(tzinfo=None)
48
- return _dt
49
-
50
-
51
32
  @router.get("/", response_model=list[JobReadV2])
52
33
  async def view_job(
53
34
  id: Optional[int] = None,
@@ -85,6 +66,14 @@ async def view_job(
85
66
  log: If `True`, include `job.log`, if `False`
86
67
  `job.log` is set to `None`.
87
68
  """
69
+
70
+ _raise_if_naive_datetime(
71
+ start_timestamp_min,
72
+ start_timestamp_max,
73
+ end_timestamp_min,
74
+ end_timestamp_max,
75
+ )
76
+
88
77
  stm = select(JobV2)
89
78
 
90
79
  if id is not None:
@@ -102,16 +91,16 @@ async def view_job(
102
91
  if status is not None:
103
92
  stm = stm.where(JobV2.status == status)
104
93
  if start_timestamp_min is not None:
105
- start_timestamp_min = _convert_to_db_timestamp(start_timestamp_min)
94
+ start_timestamp_min = start_timestamp_min
106
95
  stm = stm.where(JobV2.start_timestamp >= start_timestamp_min)
107
96
  if start_timestamp_max is not None:
108
- start_timestamp_max = _convert_to_db_timestamp(start_timestamp_max)
97
+ start_timestamp_max = start_timestamp_max
109
98
  stm = stm.where(JobV2.start_timestamp <= start_timestamp_max)
110
99
  if end_timestamp_min is not None:
111
- end_timestamp_min = _convert_to_db_timestamp(end_timestamp_min)
100
+ end_timestamp_min = end_timestamp_min
112
101
  stm = stm.where(JobV2.end_timestamp >= end_timestamp_min)
113
102
  if end_timestamp_max is not None:
114
- end_timestamp_max = _convert_to_db_timestamp(end_timestamp_max)
103
+ end_timestamp_max = end_timestamp_max
115
104
  stm = stm.where(JobV2.end_timestamp <= end_timestamp_max)
116
105
 
117
106
  res = await db.execute(stm)
@@ -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()