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,11 +1,11 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
- from typing import Literal
4
3
  from typing import Optional
5
4
 
6
5
  from pydantic import BaseModel
7
6
  from pydantic import Extra
8
7
  from pydantic import Field
8
+ from pydantic import root_validator
9
9
  from pydantic import validator
10
10
 
11
11
  from .._validators import val_absolute_path
@@ -20,6 +20,19 @@ class TaskGroupV2OriginEnum(str, Enum):
20
20
  OTHER = "other"
21
21
 
22
22
 
23
+ class TaskGroupActivityStatusV2(str, Enum):
24
+ PENDING = "pending"
25
+ ONGOING = "ongoing"
26
+ FAILED = "failed"
27
+ OK = "OK"
28
+
29
+
30
+ class TaskGroupActivityActionV2(str, Enum):
31
+ COLLECT = "collect"
32
+ DEACTIVATE = "deactivate"
33
+ REACTIVATE = "reactivate"
34
+
35
+
23
36
  class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
24
37
  user_id: int
25
38
  user_group_id: Optional[int] = None
@@ -32,6 +45,7 @@ class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
32
45
  venv_path: Optional[str] = None
33
46
  wheel_path: Optional[str] = None
34
47
  pip_extras: Optional[str] = None
48
+ pip_freeze: Optional[str] = None
35
49
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
36
50
 
37
51
  # Validators
@@ -53,6 +67,32 @@ class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
53
67
  )
54
68
 
55
69
 
70
+ class TaskGroupCreateV2Strict(TaskGroupCreateV2):
71
+ """
72
+ A strict version of TaskGroupCreateV2, to be used for task collection.
73
+ """
74
+
75
+ path: str
76
+ venv_path: str
77
+ version: str
78
+ python_version: str
79
+
80
+ @root_validator
81
+ def check_wheel_file(cls, values):
82
+ origin = values.get("origin")
83
+ wheel_path = values.get("wheel_path")
84
+ bad_condition_1 = (
85
+ origin == TaskGroupV2OriginEnum.WHEELFILE and wheel_path is None
86
+ )
87
+ bad_condition_2 = (
88
+ origin != TaskGroupV2OriginEnum.WHEELFILE
89
+ and wheel_path is not None
90
+ )
91
+ if bad_condition_1 or bad_condition_2:
92
+ raise ValueError(f"Cannot have {origin=} and {wheel_path=}.")
93
+ return values
94
+
95
+
56
96
  class TaskGroupReadV2(BaseModel):
57
97
  id: int
58
98
  task_list: list[TaskReadV2]
@@ -60,26 +100,37 @@ class TaskGroupReadV2(BaseModel):
60
100
  user_id: int
61
101
  user_group_id: Optional[int] = None
62
102
 
63
- origin: Literal["pypi", "wheel-file", "other"]
103
+ origin: TaskGroupV2OriginEnum
64
104
  pkg_name: str
65
105
  version: Optional[str] = None
66
106
  python_version: Optional[str] = None
67
107
  path: Optional[str] = None
68
108
  venv_path: Optional[str] = None
69
109
  wheel_path: Optional[str] = None
110
+ pip_freeze: Optional[str] = None
70
111
  pip_extras: Optional[str] = None
71
112
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
72
113
 
114
+ venv_size_in_kB: Optional[int] = None
115
+ venv_file_number: Optional[int] = None
116
+
73
117
  active: bool
74
118
  timestamp_created: datetime
119
+ timestamp_last_used: datetime
75
120
 
76
121
 
77
122
  class TaskGroupUpdateV2(BaseModel, extra=Extra.forbid):
78
123
  user_group_id: Optional[int] = None
79
- active: Optional[bool] = None
80
124
 
81
- @validator("active")
82
- def active_cannot_be_None(cls, value):
83
- if value is None:
84
- raise ValueError("`active` cannot be set to None")
85
- return value
125
+
126
+ class TaskGroupActivityV2Read(BaseModel):
127
+ id: int
128
+ user_id: int
129
+ taskgroupv2_id: Optional[int] = None
130
+ timestamp_started: datetime
131
+ timestamp_ended: Optional[datetime] = None
132
+ pkg_name: str
133
+ version: str
134
+ status: TaskGroupActivityStatusV2
135
+ action: TaskGroupActivityActionV2
136
+ log: Optional[str] = None
@@ -6,7 +6,6 @@ from pydantic import Extra
6
6
  from pydantic import validator
7
7
 
8
8
  from .._validators import valstr
9
- from .._validators import valutc
10
9
  from .project import ProjectReadV2
11
10
  from .workflowtask import WorkflowTaskExportV2
12
11
  from .workflowtask import WorkflowTaskImportV2
@@ -31,10 +30,6 @@ class WorkflowReadV2(BaseModel):
31
30
  project: ProjectReadV2
32
31
  timestamp_created: datetime
33
32
 
34
- _timestamp_created = validator("timestamp_created", allow_reuse=True)(
35
- valutc("timestamp_created")
36
- )
37
-
38
33
 
39
34
  class WorkflowReadV2WithWarnings(WorkflowReadV2):
40
35
  task_list: list[WorkflowTaskReadV2WithWarning]
@@ -43,6 +43,9 @@ from fastapi_users.exceptions import UserAlreadyExists
43
43
  from fastapi_users.models import ID
44
44
  from fastapi_users.models import OAP
45
45
  from fastapi_users.models import UP
46
+ from fastapi_users.password import PasswordHelper
47
+ from pwdlib import PasswordHash
48
+ from pwdlib.hashers.bcrypt import BcryptHasher
46
49
  from sqlalchemy.ext.asyncio import AsyncSession
47
50
  from sqlalchemy.orm import selectinload
48
51
  from sqlmodel import func
@@ -177,7 +180,21 @@ async def get_user_db(
177
180
  yield SQLModelUserDatabaseAsync(session, UserOAuth, OAuthAccount)
178
181
 
179
182
 
183
+ password_hash = PasswordHash(hashers=(BcryptHasher(),))
184
+ password_helper = PasswordHelper(password_hash=password_hash)
185
+
186
+
180
187
  class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
188
+ def __init__(self, user_db):
189
+ """
190
+ Override `__init__` of `BaseUserManager` to define custom
191
+ `password_helper`.
192
+ """
193
+ super().__init__(
194
+ user_db=user_db,
195
+ password_helper=password_helper,
196
+ )
197
+
181
198
  async def validate_password(self, password: str, user: UserOAuth) -> None:
182
199
  # check password length
183
200
  min_length = 4
fractal_server/config.py CHANGED
@@ -16,7 +16,6 @@ import shutil
16
16
  import sys
17
17
  from os import environ
18
18
  from os import getenv
19
- from os.path import abspath
20
19
  from pathlib import Path
21
20
  from typing import Literal
22
21
  from typing import Optional
@@ -88,7 +87,7 @@ class Settings(BaseSettings):
88
87
  """
89
88
  Contains all the configuration variables for Fractal Server
90
89
 
91
- The attributes of this class are set from the environtment.
90
+ The attributes of this class are set from the environment.
92
91
  """
93
92
 
94
93
  class Config:
@@ -167,10 +166,6 @@ class Settings(BaseSettings):
167
166
  ###########################################################################
168
167
  # DATABASE
169
168
  ###########################################################################
170
- DB_ENGINE: Literal["sqlite", "postgres-psycopg"] = "sqlite"
171
- """
172
- Database engine to use (supported: `sqlite`, `postgres-psycopg`).
173
- """
174
169
  DB_ECHO: bool = False
175
170
  """
176
171
  If `True`, make database operations verbose.
@@ -196,44 +191,21 @@ class Settings(BaseSettings):
196
191
  Name of the PostgreSQL database to connect to.
197
192
  """
198
193
 
199
- SQLITE_PATH: Optional[str]
200
- """
201
- File path where the SQLite database is located (or will be located).
202
- """
203
-
204
194
  @property
205
195
  def DATABASE_ASYNC_URL(self) -> URL:
206
- if self.DB_ENGINE == "postgres-psycopg":
207
- url = URL.create(
208
- drivername="postgresql+psycopg",
209
- username=self.POSTGRES_USER,
210
- password=self.POSTGRES_PASSWORD,
211
- host=self.POSTGRES_HOST,
212
- port=self.POSTGRES_PORT,
213
- database=self.POSTGRES_DB,
214
- )
215
- else:
216
- if not self.SQLITE_PATH:
217
- raise FractalConfigurationError(
218
- "SQLITE_PATH path cannot be None"
219
- )
220
- sqlite_path = abspath(self.SQLITE_PATH)
221
- url = URL.create(
222
- drivername="sqlite+aiosqlite",
223
- database=sqlite_path,
224
- )
196
+ url = URL.create(
197
+ drivername="postgresql+psycopg",
198
+ username=self.POSTGRES_USER,
199
+ password=self.POSTGRES_PASSWORD,
200
+ host=self.POSTGRES_HOST,
201
+ port=self.POSTGRES_PORT,
202
+ database=self.POSTGRES_DB,
203
+ )
225
204
  return url
226
205
 
227
206
  @property
228
207
  def DATABASE_SYNC_URL(self):
229
- if self.DB_ENGINE == "postgres-psycopg":
230
- return self.DATABASE_ASYNC_URL.set(drivername="postgresql+psycopg")
231
- else:
232
- if not self.SQLITE_PATH:
233
- raise FractalConfigurationError(
234
- "SQLITE_PATH path cannot be None"
235
- )
236
- return self.DATABASE_ASYNC_URL.set(drivername="sqlite")
208
+ return self.DATABASE_ASYNC_URL.set(drivername="postgresql+psycopg")
237
209
 
238
210
  ###########################################################################
239
211
  # FRACTAL SPECIFIC
@@ -411,7 +383,7 @@ class Settings(BaseSettings):
411
383
  @root_validator(pre=True)
412
384
  def check_tasks_python(cls, values) -> None:
413
385
  """
414
- Perform multiple checks of the Python-intepreter variables.
386
+ Perform multiple checks of the Python-interpreter variables.
415
387
 
416
388
  1. Each `FRACTAL_TASKS_PYTHON_X_Y` variable must be an absolute path,
417
389
  if set.
@@ -460,7 +432,7 @@ class Settings(BaseSettings):
460
432
  f"{current_version_dot}"
461
433
  )
462
434
 
463
- # Unset all existing intepreters variable
435
+ # Unset all existing interpreters variable
464
436
  for _version in ["3_9", "3_10", "3_11", "3_12"]:
465
437
  key = f"FRACTAL_TASKS_PYTHON_{_version}"
466
438
  if _version == current_version:
@@ -526,6 +498,36 @@ class Settings(BaseSettings):
526
498
  Maximum value at which to update `pip` before performing task collection.
527
499
  """
528
500
 
501
+ FRACTAL_VIEWER_AUTHORIZATION_SCHEME: Literal[
502
+ "viewer-paths", "users-folders", "none"
503
+ ] = "none"
504
+ """
505
+ Defines how the list of allowed viewer paths is built.
506
+
507
+ This variable affects the `GET /auth/current-user/allowed-viewer-paths/`
508
+ response, which is then consumed by
509
+ [fractal-vizarr-viewer](https://github.com/fractal-analytics-platform/fractal-vizarr-viewer).
510
+
511
+ Options:
512
+
513
+ - "viewer-paths": The list of allowed viewer paths will include the user's
514
+ `project_dir` along with any path defined in user groups' `viewer_paths`
515
+ attributes.
516
+ - "users-folders": The list will consist of the user's `project_dir` and a
517
+ user-specific folder. The user folder is constructed by concatenating
518
+ the base folder `FRACTAL_VIEWER_BASE_FOLDER` with the user's
519
+ `slurm_user`.
520
+ - "none": An empty list will be returned, indicating no access to
521
+ viewer paths. Useful when vizarr viewer is not used.
522
+ """
523
+
524
+ FRACTAL_VIEWER_BASE_FOLDER: Optional[str] = None
525
+ """
526
+ Base path to Zarr files that will be served by fractal-vizarr-viewer;
527
+ This variable is required and used only when
528
+ FRACTAL_VIEWER_AUTHORIZATION_SCHEME is set to "users-folders".
529
+ """
530
+
529
531
  ###########################################################################
530
532
  # BUSINESS LOGIC
531
533
  ###########################################################################
@@ -533,25 +535,8 @@ class Settings(BaseSettings):
533
535
  """
534
536
  Checks that db environment variables are properly set.
535
537
  """
536
- if self.DB_ENGINE == "postgres-psycopg":
537
- if not self.POSTGRES_DB:
538
- raise FractalConfigurationError(
539
- "POSTGRES_DB cannot be None when DB_ENGINE="
540
- "postgres-psycopg."
541
- )
542
-
543
- try:
544
- import psycopg # noqa: F401
545
- except ModuleNotFoundError:
546
- raise FractalConfigurationError(
547
- "DB engine is `postgres-psycopg` but `psycopg` is not "
548
- "available"
549
- )
550
- else:
551
- if not self.SQLITE_PATH:
552
- raise FractalConfigurationError(
553
- "SQLITE_PATH cannot be None when DB_ENGINE=sqlite."
554
- )
538
+ if not self.POSTGRES_DB:
539
+ raise FractalConfigurationError("POSTGRES_DB cannot be None.")
555
540
 
556
541
  def check_runner(self) -> None:
557
542
 
@@ -634,6 +619,23 @@ class Settings(BaseSettings):
634
619
  if not self.FRACTAL_TASKS_DIR:
635
620
  raise FractalConfigurationError("FRACTAL_TASKS_DIR cannot be None")
636
621
 
622
+ # FRACTAL_VIEWER_BASE_FOLDER is required when
623
+ # FRACTAL_VIEWER_AUTHORIZATION_SCHEME is set to "users-folders"
624
+ # and it must be an absolute path
625
+ if self.FRACTAL_VIEWER_AUTHORIZATION_SCHEME == "users-folders":
626
+ viewer_base_folder = self.FRACTAL_VIEWER_BASE_FOLDER
627
+ if viewer_base_folder is None:
628
+ raise FractalConfigurationError(
629
+ "FRACTAL_VIEWER_BASE_FOLDER is required when "
630
+ "FRACTAL_VIEWER_AUTHORIZATION_SCHEME is set to "
631
+ "users-folders"
632
+ )
633
+ if not Path(viewer_base_folder).is_absolute():
634
+ raise FractalConfigurationError(
635
+ f"Non-absolute value for "
636
+ f"FRACTAL_VIEWER_BASE_FOLDER={viewer_base_folder}"
637
+ )
638
+
637
639
  self.check_db()
638
640
  self.check_runner()
639
641
 
@@ -0,0 +1,117 @@
1
+ """TaskGroup Activity and venv-info to TaskGroup
2
+
3
+ Revision ID: d256a7379ab8
4
+ Revises: 19eca0dd47a9
5
+ Create Date: 2024-11-20 15:01:52.659832
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ import sqlmodel
10
+ from alembic import op
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "d256a7379ab8"
15
+ down_revision = "19eca0dd47a9"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table(
23
+ "taskgroupactivityv2",
24
+ sa.Column("id", sa.Integer(), nullable=False),
25
+ sa.Column("user_id", sa.Integer(), nullable=False),
26
+ sa.Column("taskgroupv2_id", sa.Integer(), nullable=True),
27
+ sa.Column(
28
+ "timestamp_started", sa.DateTime(timezone=True), nullable=False
29
+ ),
30
+ sa.Column(
31
+ "pkg_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False
32
+ ),
33
+ sa.Column(
34
+ "version", sqlmodel.sql.sqltypes.AutoString(), nullable=False
35
+ ),
36
+ sa.Column(
37
+ "status", sqlmodel.sql.sqltypes.AutoString(), nullable=False
38
+ ),
39
+ sa.Column(
40
+ "action", sqlmodel.sql.sqltypes.AutoString(), nullable=False
41
+ ),
42
+ sa.Column("log", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
43
+ sa.Column(
44
+ "timestamp_ended", sa.DateTime(timezone=True), nullable=True
45
+ ),
46
+ sa.ForeignKeyConstraint(
47
+ ["taskgroupv2_id"],
48
+ ["taskgroupv2.id"],
49
+ name=op.f("fk_taskgroupactivityv2_taskgroupv2_id_taskgroupv2"),
50
+ ),
51
+ sa.ForeignKeyConstraint(
52
+ ["user_id"],
53
+ ["user_oauth.id"],
54
+ name=op.f("fk_taskgroupactivityv2_user_id_user_oauth"),
55
+ ),
56
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_taskgroupactivityv2")),
57
+ )
58
+ op.drop_table("collectionstatev2")
59
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
60
+ batch_op.add_column(
61
+ sa.Column(
62
+ "pip_freeze", sqlmodel.sql.sqltypes.AutoString(), nullable=True
63
+ )
64
+ )
65
+ batch_op.add_column(
66
+ sa.Column("venv_size_in_kB", sa.Integer(), nullable=True)
67
+ )
68
+ batch_op.add_column(
69
+ sa.Column("venv_file_number", sa.Integer(), nullable=True)
70
+ )
71
+ batch_op.add_column(
72
+ sa.Column(
73
+ "timestamp_last_used",
74
+ sa.DateTime(timezone=True),
75
+ server_default="2024-11-20T00:00:00+00:00",
76
+ nullable=False,
77
+ )
78
+ )
79
+
80
+ # ### end Alembic commands ###
81
+
82
+
83
+ def downgrade() -> None:
84
+ # ### commands auto generated by Alembic - please adjust! ###
85
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
86
+ batch_op.drop_column("timestamp_last_used")
87
+ batch_op.drop_column("venv_file_number")
88
+ batch_op.drop_column("venv_size_in_kB")
89
+ batch_op.drop_column("pip_freeze")
90
+
91
+ op.create_table(
92
+ "collectionstatev2",
93
+ sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
94
+ sa.Column(
95
+ "data",
96
+ postgresql.JSON(astext_type=sa.Text()),
97
+ autoincrement=False,
98
+ nullable=True,
99
+ ),
100
+ sa.Column(
101
+ "timestamp",
102
+ postgresql.TIMESTAMP(timezone=True),
103
+ autoincrement=False,
104
+ nullable=True,
105
+ ),
106
+ sa.Column(
107
+ "taskgroupv2_id", sa.INTEGER(), autoincrement=False, nullable=True
108
+ ),
109
+ sa.ForeignKeyConstraint(
110
+ ["taskgroupv2_id"],
111
+ ["taskgroupv2.id"],
112
+ name="fk_collectionstatev2_taskgroupv2_id_taskgroupv2",
113
+ ),
114
+ sa.PrimaryKeyConstraint("id", name="pk_collectionstatev2"),
115
+ )
116
+ op.drop_table("taskgroupactivityv2")
117
+ # ### end Alembic commands ###