fractal-server 2.12.1__py3-none-any.whl → 2.13.1__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 (87) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +9 -12
  3. fractal_server/app/models/v2/__init__.py +4 -0
  4. fractal_server/app/models/v2/accounting.py +35 -0
  5. fractal_server/app/models/v2/dataset.py +2 -2
  6. fractal_server/app/models/v2/job.py +11 -9
  7. fractal_server/app/models/v2/task.py +2 -3
  8. fractal_server/app/models/v2/task_group.py +6 -2
  9. fractal_server/app/models/v2/workflowtask.py +15 -8
  10. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  11. fractal_server/app/routes/admin/v2/accounting.py +108 -0
  12. fractal_server/app/routes/admin/v2/impersonate.py +35 -0
  13. fractal_server/app/routes/admin/v2/job.py +5 -13
  14. fractal_server/app/routes/admin/v2/task.py +1 -1
  15. fractal_server/app/routes/admin/v2/task_group.py +5 -13
  16. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  17. fractal_server/app/routes/api/v2/dataset.py +4 -4
  18. fractal_server/app/routes/api/v2/images.py +11 -11
  19. fractal_server/app/routes/api/v2/project.py +2 -2
  20. fractal_server/app/routes/api/v2/status.py +1 -1
  21. fractal_server/app/routes/api/v2/submit.py +9 -6
  22. fractal_server/app/routes/api/v2/task.py +4 -2
  23. fractal_server/app/routes/api/v2/task_collection.py +3 -2
  24. fractal_server/app/routes/api/v2/task_group.py +4 -7
  25. fractal_server/app/routes/api/v2/workflow.py +3 -3
  26. fractal_server/app/routes/api/v2/workflow_import.py +3 -3
  27. fractal_server/app/routes/api/v2/workflowtask.py +3 -1
  28. fractal_server/app/routes/auth/_aux_auth.py +4 -1
  29. fractal_server/app/routes/auth/current_user.py +3 -5
  30. fractal_server/app/routes/auth/group.py +1 -1
  31. fractal_server/app/routes/auth/users.py +2 -4
  32. fractal_server/app/routes/aux/__init__.py +0 -20
  33. fractal_server/app/routes/aux/_runner.py +1 -1
  34. fractal_server/app/routes/aux/validate_user_settings.py +1 -2
  35. fractal_server/app/runner/executors/_job_states.py +13 -0
  36. fractal_server/app/runner/executors/slurm/_slurm_config.py +26 -18
  37. fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -3
  38. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +31 -22
  39. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +2 -5
  40. fractal_server/app/runner/executors/slurm/ssh/executor.py +21 -27
  41. fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -3
  42. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +1 -2
  43. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +37 -47
  44. fractal_server/app/runner/executors/slurm/sudo/executor.py +25 -24
  45. fractal_server/app/runner/v2/__init__.py +4 -9
  46. fractal_server/app/runner/v2/_local/__init__.py +3 -0
  47. fractal_server/app/runner/v2/_local/_local_config.py +5 -4
  48. fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +4 -4
  49. fractal_server/app/runner/v2/_slurm_ssh/__init__.py +2 -0
  50. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +4 -2
  51. fractal_server/app/runner/v2/deduplicate_list.py +1 -1
  52. fractal_server/app/runner/v2/runner.py +25 -10
  53. fractal_server/app/runner/v2/runner_functions.py +12 -11
  54. fractal_server/app/runner/v2/task_interface.py +15 -7
  55. fractal_server/app/schemas/_filter_validators.py +6 -3
  56. fractal_server/app/schemas/_validators.py +7 -5
  57. fractal_server/app/schemas/user.py +23 -18
  58. fractal_server/app/schemas/user_group.py +25 -11
  59. fractal_server/app/schemas/user_settings.py +31 -24
  60. fractal_server/app/schemas/v2/__init__.py +1 -0
  61. fractal_server/app/schemas/v2/accounting.py +18 -0
  62. fractal_server/app/schemas/v2/dataset.py +48 -35
  63. fractal_server/app/schemas/v2/dumps.py +16 -14
  64. fractal_server/app/schemas/v2/job.py +49 -29
  65. fractal_server/app/schemas/v2/manifest.py +32 -28
  66. fractal_server/app/schemas/v2/project.py +18 -8
  67. fractal_server/app/schemas/v2/task.py +86 -75
  68. fractal_server/app/schemas/v2/task_collection.py +41 -30
  69. fractal_server/app/schemas/v2/task_group.py +39 -20
  70. fractal_server/app/schemas/v2/workflow.py +24 -12
  71. fractal_server/app/schemas/v2/workflowtask.py +63 -61
  72. fractal_server/app/security/__init__.py +1 -1
  73. fractal_server/config.py +86 -73
  74. fractal_server/images/models.py +18 -12
  75. fractal_server/main.py +1 -1
  76. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
  77. fractal_server/tasks/v2/utils_background.py +2 -2
  78. fractal_server/tasks/v2/utils_database.py +1 -1
  79. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/METADATA +9 -10
  80. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/RECORD +83 -81
  81. fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -121
  82. fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -108
  83. fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -42
  84. fractal_server/app/runner/v2/_local_experimental/executor.py +0 -157
  85. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/LICENSE +0 -0
  86. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/WHEEL +0 -0
  87. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.12.1"
1
+ __VERSION__ = "2.13.1"
@@ -12,6 +12,7 @@
12
12
  from datetime import datetime
13
13
  from typing import Optional
14
14
 
15
+ from pydantic import ConfigDict
15
16
  from pydantic import EmailStr
16
17
  from sqlalchemy import Column
17
18
  from sqlalchemy.types import DateTime
@@ -50,13 +51,11 @@ class OAuthAccount(SQLModel, table=True):
50
51
  user: Optional["UserOAuth"] = Relationship(back_populates="oauth_accounts")
51
52
  oauth_name: str = Field(index=True, nullable=False)
52
53
  access_token: str = Field(nullable=False)
53
- expires_at: Optional[int] = Field(nullable=True)
54
- refresh_token: Optional[str] = Field(nullable=True)
54
+ expires_at: Optional[int] = Field(nullable=True, default=None)
55
+ refresh_token: Optional[str] = Field(nullable=True, default=None)
55
56
  account_id: str = Field(index=True, nullable=False)
56
57
  account_email: str = Field(nullable=False)
57
-
58
- class Config:
59
- orm_mode = True
58
+ model_config = ConfigDict(from_attributes=True)
60
59
 
61
60
 
62
61
  class UserOAuth(SQLModel, table=True):
@@ -88,11 +87,11 @@ class UserOAuth(SQLModel, table=True):
88
87
  sa_column_kwargs={"unique": True, "index": True}, nullable=False
89
88
  )
90
89
  hashed_password: str
91
- is_active: bool = Field(True, nullable=False)
92
- is_superuser: bool = Field(False, nullable=False)
93
- is_verified: bool = Field(False, nullable=False)
90
+ is_active: bool = Field(default=True, nullable=False)
91
+ is_superuser: bool = Field(default=False, nullable=False)
92
+ is_verified: bool = Field(default=False, nullable=False)
94
93
 
95
- username: Optional[str]
94
+ username: Optional[str] = None
96
95
 
97
96
  oauth_accounts: list["OAuthAccount"] = Relationship(
98
97
  back_populates="user",
@@ -105,9 +104,7 @@ class UserOAuth(SQLModel, table=True):
105
104
  settings: Optional[UserSettings] = Relationship(
106
105
  sa_relationship_kwargs=dict(lazy="selectin", cascade="all, delete")
107
106
  )
108
-
109
- class Config:
110
- orm_mode = True
107
+ model_config = ConfigDict(from_attributes=True)
111
108
 
112
109
 
113
110
  class UserGroup(SQLModel, table=True):
@@ -2,6 +2,8 @@
2
2
  v2 `models` module
3
3
  """
4
4
  from ..linkuserproject import LinkUserProjectV2
5
+ from .accounting import AccountingRecord
6
+ from .accounting import AccountingRecordSlurm
5
7
  from .dataset import DatasetV2
6
8
  from .job import JobV2
7
9
  from .project import ProjectV2
@@ -12,6 +14,8 @@ from .workflow import WorkflowV2
12
14
  from .workflowtask import WorkflowTaskV2
13
15
 
14
16
  __all__ = [
17
+ "AccountingRecord",
18
+ "AccountingRecordSlurm",
15
19
  "LinkUserProjectV2",
16
20
  "DatasetV2",
17
21
  "JobV2",
@@ -0,0 +1,35 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from sqlalchemy import Column
5
+ from sqlalchemy import Integer
6
+ from sqlalchemy.dialects.postgresql import ARRAY
7
+ from sqlalchemy.types import DateTime
8
+ from sqlmodel import Field
9
+ from sqlmodel import SQLModel
10
+
11
+ from ....utils import get_timestamp
12
+
13
+
14
+ class AccountingRecord(SQLModel, table=True):
15
+ id: Optional[int] = Field(default=None, primary_key=True)
16
+ user_id: int = Field(foreign_key="user_oauth.id", nullable=False)
17
+ timestamp: datetime = Field(
18
+ default_factory=get_timestamp,
19
+ sa_column=Column(DateTime(timezone=True), nullable=False),
20
+ )
21
+ num_tasks: int
22
+ num_new_images: int
23
+
24
+
25
+ class AccountingRecordSlurm(SQLModel, table=True):
26
+ id: Optional[int] = Field(default=None, primary_key=True)
27
+ user_id: int = Field(foreign_key="user_oauth.id", nullable=False)
28
+ timestamp: datetime = Field(
29
+ default_factory=get_timestamp,
30
+ sa_column=Column(DateTime(timezone=True), nullable=False),
31
+ )
32
+ slurm_job_ids: list[int] = Field(
33
+ default_factory=list,
34
+ sa_column=Column(ARRAY(Integer)),
35
+ )
@@ -2,6 +2,7 @@ from datetime import datetime
2
2
  from typing import Any
3
3
  from typing import Optional
4
4
 
5
+ from pydantic import ConfigDict
5
6
  from sqlalchemy import Column
6
7
  from sqlalchemy.types import DateTime
7
8
  from sqlalchemy.types import JSON
@@ -14,8 +15,7 @@ from fractal_server.images.models import AttributeFiltersType
14
15
 
15
16
 
16
17
  class DatasetV2(SQLModel, table=True):
17
- class Config:
18
- arbitrary_types_allowed = True
18
+ model_config = ConfigDict(arbitrary_types_allowed=True)
19
19
 
20
20
  id: Optional[int] = Field(default=None, primary_key=True)
21
21
  name: str
@@ -2,6 +2,7 @@ from datetime import datetime
2
2
  from typing import Any
3
3
  from typing import Optional
4
4
 
5
+ from pydantic import ConfigDict
5
6
  from sqlalchemy import Column
6
7
  from sqlalchemy.types import DateTime
7
8
  from sqlalchemy.types import JSON
@@ -14,16 +15,17 @@ from fractal_server.images.models import AttributeFiltersType
14
15
 
15
16
 
16
17
  class JobV2(SQLModel, table=True):
17
- class Config:
18
- arbitrary_types_allowed = True
18
+ model_config = ConfigDict(arbitrary_types_allowed=True)
19
19
 
20
20
  id: Optional[int] = Field(default=None, primary_key=True)
21
- project_id: Optional[int] = Field(foreign_key="projectv2.id")
22
- workflow_id: Optional[int] = Field(foreign_key="workflowv2.id")
23
- dataset_id: Optional[int] = Field(foreign_key="datasetv2.id")
21
+ project_id: Optional[int] = Field(foreign_key="projectv2.id", default=None)
22
+ workflow_id: Optional[int] = Field(
23
+ foreign_key="workflowv2.id", default=None
24
+ )
25
+ dataset_id: Optional[int] = Field(foreign_key="datasetv2.id", default=None)
24
26
 
25
27
  user_email: str = Field(nullable=False)
26
- slurm_account: Optional[str]
28
+ slurm_account: Optional[str] = None
27
29
 
28
30
  dataset_dump: dict[str, Any] = Field(
29
31
  sa_column=Column(JSON, nullable=False)
@@ -35,9 +37,9 @@ class JobV2(SQLModel, table=True):
35
37
  sa_column=Column(JSON, nullable=False)
36
38
  )
37
39
 
38
- worker_init: Optional[str]
39
- working_dir: Optional[str]
40
- working_dir_user: Optional[str]
40
+ worker_init: Optional[str] = None
41
+ working_dir: Optional[str] = None
42
+ working_dir_user: Optional[str] = None
41
43
  first_task_index: int
42
44
  last_task_index: int
43
45
 
@@ -1,7 +1,6 @@
1
1
  from typing import Any
2
2
  from typing import Optional
3
3
 
4
- from pydantic import HttpUrl
5
4
  from sqlalchemy import Column
6
5
  from sqlalchemy.types import JSON
7
6
  from sqlmodel import Field
@@ -31,9 +30,9 @@ class TaskV2(SQLModel, table=True):
31
30
  args_schema_parallel: Optional[dict[str, Any]] = Field(
32
31
  sa_column=Column(JSON), default=None
33
32
  )
34
- args_schema_version: Optional[str]
33
+ args_schema_version: Optional[str] = None
35
34
  docs_info: Optional[str] = None
36
- docs_link: Optional[HttpUrl] = None
35
+ docs_link: Optional[str] = None
37
36
 
38
37
  input_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
39
38
  output_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
@@ -22,7 +22,9 @@ class TaskGroupV2(SQLModel, table=True):
22
22
  )
23
23
 
24
24
  user_id: int = Field(foreign_key="user_oauth.id")
25
- user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
25
+ user_group_id: Optional[int] = Field(
26
+ foreign_key="usergroup.id", default=None
27
+ )
26
28
 
27
29
  origin: str
28
30
  pkg_name: str
@@ -97,7 +99,9 @@ class TaskGroupActivityV2(SQLModel, table=True):
97
99
 
98
100
  id: Optional[int] = Field(default=None, primary_key=True)
99
101
  user_id: int = Field(foreign_key="user_oauth.id")
100
- taskgroupv2_id: Optional[int] = Field(foreign_key="taskgroupv2.id")
102
+ taskgroupv2_id: Optional[int] = Field(
103
+ default=None, foreign_key="taskgroupv2.id"
104
+ )
101
105
  timestamp_started: datetime = Field(
102
106
  default_factory=get_timestamp,
103
107
  sa_column=Column(DateTime(timezone=True), nullable=False),
@@ -1,6 +1,7 @@
1
1
  from typing import Any
2
2
  from typing import Optional
3
3
 
4
+ from pydantic import ConfigDict
4
5
  from sqlalchemy import Column
5
6
  from sqlalchemy.types import JSON
6
7
  from sqlmodel import Field
@@ -11,18 +12,24 @@ from .task import TaskV2
11
12
 
12
13
 
13
14
  class WorkflowTaskV2(SQLModel, table=True):
14
- class Config:
15
- arbitrary_types_allowed = True
16
- fields = {"parent": {"exclude": True}}
15
+ model_config = ConfigDict(arbitrary_types_allowed=True)
17
16
 
18
17
  id: Optional[int] = Field(default=None, primary_key=True)
19
18
 
20
19
  workflow_id: int = Field(foreign_key="workflowv2.id")
21
- order: Optional[int]
22
- meta_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
23
- meta_non_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
24
- args_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
25
- args_non_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
20
+ order: Optional[int] = None
21
+ meta_parallel: Optional[dict[str, Any]] = Field(
22
+ sa_column=Column(JSON), default=None
23
+ )
24
+ meta_non_parallel: Optional[dict[str, Any]] = Field(
25
+ sa_column=Column(JSON), default=None
26
+ )
27
+ args_parallel: Optional[dict[str, Any]] = Field(
28
+ sa_column=Column(JSON), default=None
29
+ )
30
+ args_non_parallel: Optional[dict[str, Any]] = Field(
31
+ sa_column=Column(JSON), default=None
32
+ )
26
33
 
27
34
  type_filters: dict[str, bool] = Field(
28
35
  sa_column=Column(JSON, nullable=False, server_default="{}")
@@ -3,6 +3,8 @@
3
3
  """
4
4
  from fastapi import APIRouter
5
5
 
6
+ from .accounting import router as accounting_router
7
+ from .impersonate import router as impersonate_router
6
8
  from .job import router as job_router
7
9
  from .project import router as project_router
8
10
  from .task import router as task_router
@@ -11,6 +13,7 @@ from .task_group_lifecycle import router as task_group_lifecycle_router
11
13
 
12
14
  router_admin_v2 = APIRouter()
13
15
 
16
+ router_admin_v2.include_router(accounting_router, prefix="/accounting")
14
17
  router_admin_v2.include_router(job_router, prefix="/job")
15
18
  router_admin_v2.include_router(project_router, prefix="/project")
16
19
  router_admin_v2.include_router(task_router, prefix="/task")
@@ -18,3 +21,4 @@ router_admin_v2.include_router(task_group_router, prefix="/task-group")
18
21
  router_admin_v2.include_router(
19
22
  task_group_lifecycle_router, prefix="/task-group"
20
23
  )
24
+ router_admin_v2.include_router(impersonate_router, prefix="/impersonate")
@@ -0,0 +1,108 @@
1
+ from itertools import chain
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter
5
+ from fastapi import Depends
6
+ from fastapi import HTTPException
7
+ from fastapi import Query
8
+ from fastapi import status
9
+ from fastapi.responses import JSONResponse
10
+ from pydantic import BaseModel
11
+ from pydantic.types import AwareDatetime
12
+ from sqlmodel import func
13
+ from sqlmodel import select
14
+
15
+ from fractal_server.app.db import AsyncSession
16
+ from fractal_server.app.db import get_async_db
17
+ from fractal_server.app.models import UserOAuth
18
+ from fractal_server.app.models.v2 import AccountingRecord
19
+ from fractal_server.app.models.v2 import AccountingRecordSlurm
20
+ from fractal_server.app.routes.auth import current_active_superuser
21
+ from fractal_server.app.schemas.v2 import AccountingRecordRead
22
+
23
+
24
+ class AccountingQuery(BaseModel):
25
+ user_id: Optional[int] = None
26
+ timestamp_min: Optional[AwareDatetime] = None
27
+ timestamp_max: Optional[AwareDatetime] = None
28
+
29
+
30
+ class AccountingPage(BaseModel):
31
+ total_count: int
32
+ page_size: int
33
+ current_page: int
34
+ records: list[AccountingRecordRead]
35
+
36
+
37
+ router = APIRouter()
38
+
39
+
40
+ @router.post("/", response_model=AccountingPage)
41
+ async def query_accounting(
42
+ query: AccountingQuery,
43
+ # pagination
44
+ page: int = Query(default=1, ge=1),
45
+ page_size: Optional[int] = Query(default=None, ge=1),
46
+ # dependencies
47
+ superuser: UserOAuth = Depends(current_active_superuser),
48
+ db: AsyncSession = Depends(get_async_db),
49
+ ) -> AccountingPage:
50
+
51
+ if page_size is None and page > 1:
52
+ raise HTTPException(
53
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
54
+ detail=(f"Invalid pagination parameters: {page=}, {page_size=}."),
55
+ )
56
+
57
+ stm = select(AccountingRecord).order_by(AccountingRecord.id)
58
+ stm_count = select(func.count(AccountingRecord.id))
59
+ if query.user_id is not None:
60
+ stm = stm.where(AccountingRecord.user_id == query.user_id)
61
+ stm_count = stm_count.where(AccountingRecord.user_id == query.user_id)
62
+ if query.timestamp_min is not None:
63
+ stm = stm.where(AccountingRecord.timestamp >= query.timestamp_min)
64
+ stm_count = stm_count.where(
65
+ AccountingRecord.timestamp >= query.timestamp_min
66
+ )
67
+ if query.timestamp_max is not None:
68
+ stm = stm.where(AccountingRecord.timestamp <= query.timestamp_max)
69
+ stm_count = stm_count.where(
70
+ AccountingRecord.timestamp <= query.timestamp_max
71
+ )
72
+ if page_size is not None:
73
+ stm = stm.offset((page - 1) * page_size).limit(page_size)
74
+
75
+ res = await db.execute(stm)
76
+ records = res.scalars().all()
77
+ res_total_count = await db.execute(stm_count)
78
+ total_count = res_total_count.scalar()
79
+
80
+ actual_page_size = page_size or len(records)
81
+ return AccountingPage(
82
+ total_count=total_count,
83
+ page_size=actual_page_size,
84
+ current_page=page,
85
+ records=[record.model_dump() for record in records],
86
+ )
87
+
88
+
89
+ @router.post("/slurm/")
90
+ async def query_accounting_slurm(
91
+ query: AccountingQuery,
92
+ # dependencies
93
+ superuser: UserOAuth = Depends(current_active_superuser),
94
+ db: AsyncSession = Depends(get_async_db),
95
+ ) -> JSONResponse:
96
+
97
+ stm = select(AccountingRecordSlurm.slurm_job_ids)
98
+ if query.user_id is not None:
99
+ stm = stm.where(AccountingRecordSlurm.user_id == query.user_id)
100
+ if query.timestamp_min is not None:
101
+ stm = stm.where(AccountingRecordSlurm.timestamp >= query.timestamp_min)
102
+ if query.timestamp_max is not None:
103
+ stm = stm.where(AccountingRecordSlurm.timestamp <= query.timestamp_max)
104
+
105
+ res = await db.execute(stm)
106
+ nested_slurm_job_ids = res.scalars().all()
107
+ aggregated_slurm_job_ids = list(chain(*nested_slurm_job_ids))
108
+ return JSONResponse(content=aggregated_slurm_job_ids, status_code=200)
@@ -0,0 +1,35 @@
1
+ from fastapi import APIRouter
2
+ from fastapi import Depends
3
+ from fastapi.responses import JSONResponse
4
+ from fastapi_users.authentication import JWTStrategy
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 UserOAuth
9
+ from fractal_server.app.routes.auth import current_active_superuser
10
+ from fractal_server.app.routes.auth._aux_auth import _user_or_404
11
+ from fractal_server.config import get_settings
12
+ from fractal_server.syringe import Inject
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/{user_id}/")
18
+ async def impersonate_user(
19
+ user_id: int,
20
+ superuser: UserOAuth = Depends(current_active_superuser),
21
+ db: AsyncSession = Depends(get_async_db),
22
+ ) -> JSONResponse:
23
+ user = await _user_or_404(user_id, db)
24
+
25
+ settings = Inject(get_settings)
26
+ jwt_strategy = JWTStrategy(
27
+ secret=settings.JWT_SECRET_KEY, # type: ignore
28
+ lifetime_seconds=7200, # 2 hours
29
+ )
30
+ token = await jwt_strategy.write_token(user)
31
+
32
+ return JSONResponse(
33
+ content={"access_token": token, "token_type": "bearer"},
34
+ status_code=200,
35
+ )
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from pathlib import Path
3
2
  from typing import Optional
4
3
 
@@ -8,6 +7,7 @@ from fastapi import HTTPException
8
7
  from fastapi import Response
9
8
  from fastapi import status
10
9
  from fastapi.responses import StreamingResponse
10
+ from pydantic.types import AwareDatetime
11
11
  from sqlmodel import select
12
12
 
13
13
  from fractal_server.app.db import AsyncSession
@@ -16,7 +16,6 @@ from fractal_server.app.models import UserOAuth
16
16
  from fractal_server.app.models.v2 import JobV2
17
17
  from fractal_server.app.models.v2 import ProjectV2
18
18
  from fractal_server.app.routes.auth import current_active_superuser
19
- from fractal_server.app.routes.aux import _raise_if_naive_datetime
20
19
  from fractal_server.app.routes.aux._job import _write_shutdown_file
21
20
  from fractal_server.app.routes.aux._runner import _check_shutdown_is_supported
22
21
  from fractal_server.app.runner.filenames import WORKFLOW_LOG_FILENAME
@@ -37,10 +36,10 @@ async def view_job(
37
36
  dataset_id: Optional[int] = None,
38
37
  workflow_id: Optional[int] = None,
39
38
  status: Optional[JobStatusTypeV2] = None,
40
- start_timestamp_min: Optional[datetime] = None,
41
- start_timestamp_max: Optional[datetime] = None,
42
- end_timestamp_min: Optional[datetime] = None,
43
- end_timestamp_max: Optional[datetime] = None,
39
+ start_timestamp_min: Optional[AwareDatetime] = None,
40
+ start_timestamp_max: Optional[AwareDatetime] = None,
41
+ end_timestamp_min: Optional[AwareDatetime] = None,
42
+ end_timestamp_max: Optional[AwareDatetime] = None,
44
43
  log: bool = True,
45
44
  user: UserOAuth = Depends(current_active_superuser),
46
45
  db: AsyncSession = Depends(get_async_db),
@@ -67,13 +66,6 @@ async def view_job(
67
66
  `job.log` is set to `None`.
68
67
  """
69
68
 
70
- _raise_if_naive_datetime(
71
- start_timestamp_min,
72
- start_timestamp_max,
73
- end_timestamp_min,
74
- end_timestamp_max,
75
- )
76
-
77
69
  stm = select(JobV2)
78
70
 
79
71
  if id is not None:
@@ -28,7 +28,7 @@ class TaskV2Minimal(BaseModel):
28
28
  type: str
29
29
  taskgroupv2_id: int
30
30
  command_non_parallel: Optional[str] = None
31
- command_parallel: Optional[str]
31
+ command_parallel: Optional[str] = None
32
32
  source: Optional[str] = None
33
33
  version: Optional[str] = None
34
34
 
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from typing import Optional
3
2
 
4
3
  from fastapi import APIRouter
@@ -6,6 +5,7 @@ from fastapi import Depends
6
5
  from fastapi import HTTPException
7
6
  from fastapi import Response
8
7
  from fastapi import status
8
+ from pydantic.types import AwareDatetime
9
9
  from sqlalchemy.sql.operators import is_
10
10
  from sqlalchemy.sql.operators import is_not
11
11
  from sqlmodel import select
@@ -20,7 +20,6 @@ from fractal_server.app.routes.auth import current_active_superuser
20
20
  from fractal_server.app.routes.auth._aux_auth import (
21
21
  _verify_user_belongs_to_group,
22
22
  )
23
- from fractal_server.app.routes.aux import _raise_if_naive_datetime
24
23
  from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
25
24
  from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
26
25
  from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
@@ -42,13 +41,11 @@ async def get_task_group_activity_list(
42
41
  pkg_name: Optional[str] = None,
43
42
  status: Optional[TaskGroupActivityStatusV2] = None,
44
43
  action: Optional[TaskGroupActivityActionV2] = None,
45
- timestamp_started_min: Optional[datetime] = None,
44
+ timestamp_started_min: Optional[AwareDatetime] = None,
46
45
  superuser: UserOAuth = Depends(current_active_superuser),
47
46
  db: AsyncSession = Depends(get_async_db),
48
47
  ) -> list[TaskGroupActivityV2Read]:
49
48
 
50
- _raise_if_naive_datetime(timestamp_started_min)
51
-
52
49
  stm = select(TaskGroupActivityV2)
53
50
  if task_group_activity_id is not None:
54
51
  stm = stm.where(TaskGroupActivityV2.id == task_group_activity_id)
@@ -96,19 +93,14 @@ async def query_task_group_list(
96
93
  active: Optional[bool] = None,
97
94
  pkg_name: Optional[str] = None,
98
95
  origin: Optional[TaskGroupV2OriginEnum] = None,
99
- timestamp_last_used_min: Optional[datetime] = None,
100
- timestamp_last_used_max: Optional[datetime] = None,
96
+ timestamp_last_used_min: Optional[AwareDatetime] = None,
97
+ timestamp_last_used_max: Optional[AwareDatetime] = None,
101
98
  user: UserOAuth = Depends(current_active_superuser),
102
99
  db: AsyncSession = Depends(get_async_db),
103
100
  ) -> list[TaskGroupReadV2]:
104
101
 
105
102
  stm = select(TaskGroupV2)
106
103
 
107
- _raise_if_naive_datetime(
108
- timestamp_last_used_max,
109
- timestamp_last_used_min,
110
- )
111
-
112
104
  if user_group_id is not None and private is True:
113
105
  raise HTTPException(
114
106
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -163,7 +155,7 @@ async def patch_task_group(
163
155
  detail=f"TaskGroupV2 {task_group_id} not found",
164
156
  )
165
157
 
166
- for key, value in task_group_update.dict(exclude_unset=True).items():
158
+ for key, value in task_group_update.model_dump(exclude_unset=True).items():
167
159
  if (key == "user_group_id") and (value is not None):
168
160
  await _verify_user_belongs_to_group(
169
161
  user_id=user.id, user_group_id=value, db=db
@@ -55,7 +55,7 @@ async def get_package_version_from_pypi(
55
55
  f"A TimeoutException occurred while getting {url}.\n"
56
56
  f"Original error: {str(e)}."
57
57
  )
58
- logger.error(error_msg)
58
+ logger.warning(error_msg)
59
59
  raise HTTPException(
60
60
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
61
61
  detail=error_msg,
@@ -65,7 +65,7 @@ async def get_package_version_from_pypi(
65
65
  f"An unknown error occurred while getting {url}. "
66
66
  f"Original error: {str(e)}."
67
67
  )
68
- logger.error(error_msg)
68
+ logger.warning(error_msg)
69
69
  raise HTTPException(
70
70
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
71
71
  detail=error_msg,
@@ -85,7 +85,7 @@ async def get_package_version_from_pypi(
85
85
  latest_version = response_data["info"]["version"]
86
86
  available_releases = response_data["releases"].keys()
87
87
  except KeyError as e:
88
- logger.error(
88
+ logger.warning(
89
89
  f"A KeyError occurred while getting {url}. "
90
90
  f"Original error: {str(e)}."
91
91
  )
@@ -60,7 +60,7 @@ async def create_dataset(
60
60
  db_dataset = DatasetV2(
61
61
  project_id=project_id,
62
62
  zarr_dir="__PLACEHOLDER__",
63
- **dataset.dict(exclude={"zarr_dir"}),
63
+ **dataset.model_dump(exclude={"zarr_dir"}),
64
64
  )
65
65
  db.add(db_dataset)
66
66
  await db.commit()
@@ -77,7 +77,7 @@ async def create_dataset(
77
77
  await db.commit()
78
78
  await db.refresh(db_dataset)
79
79
  else:
80
- db_dataset = DatasetV2(project_id=project_id, **dataset.dict())
80
+ db_dataset = DatasetV2(project_id=project_id, **dataset.model_dump())
81
81
  db.add(db_dataset)
82
82
  await db.commit()
83
83
  await db.refresh(db_dataset)
@@ -172,7 +172,7 @@ async def update_dataset(
172
172
  ),
173
173
  )
174
174
 
175
- for key, value in dataset_update.dict(exclude_unset=True).items():
175
+ for key, value in dataset_update.model_dump(exclude_unset=True).items():
176
176
  setattr(db_dataset, key, value)
177
177
 
178
178
  await db.commit()
@@ -316,7 +316,7 @@ async def import_dataset(
316
316
  # Create new Dataset
317
317
  db_dataset = DatasetV2(
318
318
  project_id=project_id,
319
- **dataset.dict(exclude_none=True),
319
+ **dataset.model_dump(exclude_none=True),
320
320
  )
321
321
  db.add(db_dataset)
322
322
  await db.commit()
@@ -8,8 +8,8 @@ from fastapi import Response
8
8
  from fastapi import status
9
9
  from pydantic import BaseModel
10
10
  from pydantic import Field
11
- from pydantic import root_validator
12
- from pydantic import validator
11
+ from pydantic import field_validator
12
+ from pydantic import model_validator
13
13
  from sqlalchemy.orm.attributes import flag_modified
14
14
 
15
15
  from ._aux_functions import _get_dataset_check_owner
@@ -44,18 +44,18 @@ class ImagePage(BaseModel):
44
44
 
45
45
 
46
46
  class ImageQuery(BaseModel):
47
- zarr_url: Optional[str]
47
+ zarr_url: Optional[str] = None
48
48
  type_filters: dict[str, bool] = Field(default_factory=dict)
49
49
  attribute_filters: AttributeFiltersType = Field(default_factory=dict)
50
50
 
51
- _dict_keys = root_validator(pre=True, allow_reuse=True)(
52
- root_validate_dict_keys
51
+ _dict_keys = model_validator(mode="before")(
52
+ classmethod(root_validate_dict_keys)
53
53
  )
54
- _type_filters = validator("type_filters", allow_reuse=True)(
55
- validate_type_filters
54
+ _type_filters = field_validator("type_filters")(
55
+ classmethod(validate_type_filters)
56
56
  )
57
- _attribute_filters = validator("attribute_filters", allow_reuse=True)(
58
- validate_attribute_filters
57
+ _attribute_filters = field_validator("attribute_filters")(
58
+ classmethod(validate_attribute_filters)
59
59
  )
60
60
 
61
61
 
@@ -102,7 +102,7 @@ async def post_new_image(
102
102
  ),
103
103
  )
104
104
 
105
- dataset.images.append(new_image.dict())
105
+ dataset.images.append(new_image.model_dump())
106
106
  flag_modified(dataset, "images")
107
107
 
108
108
  await db.commit()
@@ -278,7 +278,7 @@ async def patch_dataset_image(
278
278
  )
279
279
  index = ret["index"]
280
280
 
281
- for key, value in image_update.dict(
281
+ for key, value in image_update.model_dump(
282
282
  exclude_none=True, exclude={"zarr_url"}
283
283
  ).items():
284
284
  db_dataset.images[index][key] = value