fractal-server 2.17.1a0__py3-none-any.whl → 2.17.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __VERSION__ = "2.17.1a0"
1
+ __VERSION__ = "2.17.1a1"
@@ -6,5 +6,4 @@ will not be picked up by alembic.
6
6
  from .linkusergroup import LinkUserGroup # noqa: F401
7
7
  from .linkuserproject import LinkUserProjectV2 # noqa: F401
8
8
  from .security import * # noqa
9
- from .user_settings import UserSettings # noqa
10
9
  from .v2 import * # noqa
@@ -95,24 +95,11 @@ class UserOAuth(SQLModel, table=True):
95
95
  ondelete="RESTRICT",
96
96
  )
97
97
 
98
- # TODO-2.17.1: update to `project_dir: str`
99
- project_dir: str = Field(
100
- sa_column=Column(
101
- String,
102
- server_default="/PLACEHOLDER",
103
- nullable=False,
104
- )
105
- )
98
+ project_dir: str
106
99
  slurm_accounts: list[str] = Field(
107
100
  sa_column=Column(ARRAY(String), server_default="{}"),
108
101
  )
109
102
 
110
- # TODO-2.17.1: remove
111
- user_settings_id: int | None = Field(
112
- foreign_key="user_settings.id",
113
- default=None,
114
- )
115
-
116
103
 
117
104
  class UserGroup(SQLModel, table=True):
118
105
  """
@@ -15,10 +15,7 @@ class ProjectV2(SQLModel, table=True):
15
15
  id: int | None = Field(default=None, primary_key=True)
16
16
  name: str
17
17
 
18
- # TODO-2.17.1: make `resource_id` not nullable
19
- resource_id: int | None = Field(
20
- foreign_key="resource.id", default=None, ondelete="RESTRICT"
21
- )
18
+ resource_id: int = Field(foreign_key="resource.id", ondelete="RESTRICT")
22
19
  timestamp_created: datetime = Field(
23
20
  default_factory=get_timestamp,
24
21
  sa_column=Column(DateTime(timezone=True), nullable=False),
@@ -42,10 +42,7 @@ class TaskGroupV2(SQLModel, table=True):
42
42
  user_group_id: int | None = Field(
43
43
  foreign_key="usergroup.id", default=None, ondelete="SET NULL"
44
44
  )
45
- # TODO-2.17.1: make `resource_id` not nullable
46
- resource_id: int | None = Field(
47
- foreign_key="resource.id", default=None, ondelete="RESTRICT"
48
- )
45
+ resource_id: int = Field(foreign_key="resource.id", ondelete="RESTRICT")
49
46
 
50
47
  origin: str
51
48
  pkg_name: str
@@ -12,44 +12,48 @@ from fractal_server.syringe import Inject
12
12
 
13
13
 
14
14
  async def cleanup_after_shutdown(*, jobsV2: list[int], logger_name: str):
15
+ settings = Inject(get_settings)
15
16
  logger = get_logger(logger_name)
16
17
  logger.info("Cleanup function after shutdown")
17
- stm_v2 = (
18
+ stm_objects = (
18
19
  select(JobV2)
19
20
  .where(JobV2.id.in_(jobsV2))
20
21
  .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
21
22
  )
23
+ stm_ids = (
24
+ select(JobV2.id)
25
+ .where(JobV2.id.in_(jobsV2))
26
+ .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
27
+ )
22
28
 
23
29
  async for session in get_async_db():
24
- jobsV2_db = (await session.execute(stm_v2)).scalars().all()
25
-
26
- for job in jobsV2_db:
30
+ # Write shutdown file for all jobs
31
+ jobs = (await session.execute(stm_objects)).scalars().all()
32
+ for job in jobs:
27
33
  _write_shutdown_file(job=job)
28
34
 
29
- settings = Inject(get_settings)
30
-
35
+ # Wait for completion of all job - with a timeout
36
+ interval = settings.FRACTAL_GRACEFUL_SHUTDOWN_TIME / 20
31
37
  t_start = time.perf_counter()
32
38
  while (
33
39
  time.perf_counter() - t_start
34
- ) < settings.FRACTAL_GRACEFUL_SHUTDOWN_TIME: # 30 seconds
35
- logger.info("Waiting 3 seconds before checking")
36
- time.sleep(3)
37
- jobsV2_db = (await session.execute(stm_v2)).scalars().all()
38
-
39
- if len(jobsV2_db) == 0:
40
- logger.info(
41
- "All jobs associated to this app are "
42
- "either done or failed. Exit."
43
- )
40
+ ) <= settings.FRACTAL_GRACEFUL_SHUTDOWN_TIME:
41
+ job_ids = (await session.execute(stm_ids)).scalars().all()
42
+ if len(job_ids) == 0:
43
+ logger.info("All jobs are either done or failed. Exit.")
44
44
  return
45
45
  else:
46
- logger.info(f"Some jobs are still 'submitted' {jobsV2_db=}")
46
+ logger.info(f"Some jobs are still 'submitted': {job_ids=}")
47
+ logger.info(f"Wait {interval:.4f} seconds before next check.")
48
+ time.sleep(interval)
47
49
  logger.info(
48
50
  "Graceful shutdown reached its maximum time, "
49
- "but some jobs are still submitted"
51
+ "but some jobs are still submitted."
50
52
  )
51
53
 
52
- for job in jobsV2_db:
54
+ # Mark jobs as failed and update their logs.
55
+ jobs = (await session.execute(stm_objects)).scalars().all()
56
+ for job in jobs:
53
57
  job.status = "failed"
54
58
  job.log = (job.log or "") + "\nJob stopped due to app shutdown\n"
55
59
  session.add(job)
@@ -57,7 +57,7 @@ class Settings(BaseSettings):
57
57
  `app.state`.
58
58
  """
59
59
 
60
- FRACTAL_GRACEFUL_SHUTDOWN_TIME: int = 30
60
+ FRACTAL_GRACEFUL_SHUTDOWN_TIME: float = 30.0
61
61
  """
62
62
  Waiting time for the shutdown phase of executors
63
63
  """
@@ -0,0 +1,46 @@
1
+ """Make resource_id FK non-nullable
2
+
3
+ Revision ID: 45fbb391d7af
4
+ Revises: caba9fb1ea5e
5
+ Create Date: 2025-11-11 16:39:12.813766
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ from alembic import op
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "45fbb391d7af"
14
+ down_revision = "caba9fb1ea5e"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table("projectv2", schema=None) as batch_op:
22
+ batch_op.alter_column(
23
+ "resource_id", existing_type=sa.INTEGER(), nullable=False
24
+ )
25
+
26
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
27
+ batch_op.alter_column(
28
+ "resource_id", existing_type=sa.INTEGER(), nullable=False
29
+ )
30
+
31
+ # ### end Alembic commands ###
32
+
33
+
34
+ def downgrade() -> None:
35
+ # ### commands auto generated by Alembic - please adjust! ###
36
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
37
+ batch_op.alter_column(
38
+ "resource_id", existing_type=sa.INTEGER(), nullable=True
39
+ )
40
+
41
+ with op.batch_alter_table("projectv2", schema=None) as batch_op:
42
+ batch_op.alter_column(
43
+ "resource_id", existing_type=sa.INTEGER(), nullable=True
44
+ )
45
+
46
+ # ### end Alembic commands ###
@@ -0,0 +1,63 @@
1
+ """Drop table
2
+
3
+ Revision ID: 49d0856e9569
4
+ Revises: 45fbb391d7af
5
+ Create Date: 2025-11-11 16:39:41.497832
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ from alembic import op
10
+ from sqlalchemy.dialects import postgresql
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "49d0856e9569"
14
+ down_revision = "45fbb391d7af"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ op.drop_table("user_settings")
22
+ # ### end Alembic commands ###
23
+
24
+
25
+ def downgrade() -> None:
26
+ # ### commands auto generated by Alembic - please adjust! ###
27
+ op.create_table(
28
+ "user_settings",
29
+ sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
30
+ sa.Column(
31
+ "slurm_accounts",
32
+ postgresql.JSONB(astext_type=sa.Text()),
33
+ server_default=sa.text("'[]'::json"),
34
+ autoincrement=False,
35
+ nullable=False,
36
+ ),
37
+ sa.Column(
38
+ "ssh_host", sa.VARCHAR(), autoincrement=False, nullable=True
39
+ ),
40
+ sa.Column(
41
+ "ssh_username", sa.VARCHAR(), autoincrement=False, nullable=True
42
+ ),
43
+ sa.Column(
44
+ "ssh_private_key_path",
45
+ sa.VARCHAR(),
46
+ autoincrement=False,
47
+ nullable=True,
48
+ ),
49
+ sa.Column(
50
+ "ssh_tasks_dir", sa.VARCHAR(), autoincrement=False, nullable=True
51
+ ),
52
+ sa.Column(
53
+ "ssh_jobs_dir", sa.VARCHAR(), autoincrement=False, nullable=True
54
+ ),
55
+ sa.Column(
56
+ "slurm_user", sa.VARCHAR(), autoincrement=False, nullable=True
57
+ ),
58
+ sa.Column(
59
+ "project_dir", sa.VARCHAR(), autoincrement=False, nullable=True
60
+ ),
61
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_user_settings")),
62
+ )
63
+ # ### end Alembic commands ###
@@ -0,0 +1,29 @@
1
+ """Remove project_dir server_default
2
+
3
+ Revision ID: 7673fe18c05d
4
+ Revises: 49d0856e9569
5
+ Create Date: 2025-11-11 16:50:20.079193
6
+
7
+ """
8
+ from alembic import op
9
+
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = "7673fe18c05d"
13
+ down_revision = "49d0856e9569"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ """
20
+ Remove `server_default` for `project_dir` column - see
21
+ https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.alter_column.params.server_default
22
+ """
23
+ with op.batch_alter_table("user_oauth") as batch_op:
24
+ batch_op.alter_column("project_dir", server_default=None)
25
+
26
+
27
+ def downgrade() -> None:
28
+ with op.batch_alter_table("user_oauth") as batch_op:
29
+ batch_op.alter_column("project_dir", server_default="/PLACEHOLDER")
@@ -0,0 +1,49 @@
1
+ """Drop UserOAuth.user_settings_id
2
+
3
+ Revision ID: caba9fb1ea5e
4
+ Revises: 83bc2ad3ffcc
5
+ Create Date: 2025-11-11 16:38:27.243693
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ from alembic import op
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "caba9fb1ea5e"
14
+ down_revision = "83bc2ad3ffcc"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table("user_oauth", schema=None) as batch_op:
22
+ batch_op.drop_constraint(
23
+ batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
24
+ type_="foreignkey",
25
+ )
26
+ batch_op.drop_column("user_settings_id")
27
+
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade() -> None:
32
+ # ### commands auto generated by Alembic - please adjust! ###
33
+ with op.batch_alter_table("user_oauth", schema=None) as batch_op:
34
+ batch_op.add_column(
35
+ sa.Column(
36
+ "user_settings_id",
37
+ sa.INTEGER(),
38
+ autoincrement=False,
39
+ nullable=True,
40
+ )
41
+ )
42
+ batch_op.create_foreign_key(
43
+ batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
44
+ "user_settings",
45
+ ["user_settings_id"],
46
+ ["id"],
47
+ )
48
+
49
+ # ### end Alembic commands ###
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fractal-server
3
- Version: 2.17.1a0
3
+ Version: 2.17.1a1
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -1,23 +1,22 @@
1
- fractal_server/__init__.py,sha256=DW4yg1QyP_AsgqW1m4v28pnbhBE5KzkBv39LnIslJnE,25
1
+ fractal_server/__init__.py,sha256=q8l8BntWJPUSkWbFRN9cZn0xrmXCLBoe5spKmWSrjxM,25
2
2
  fractal_server/__main__.py,sha256=qLbUicU1Ulaob_Eo5pspi-IH2xAkLfifJTH9gYEhZss,11427
3
3
  fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
4
4
  fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  fractal_server/app/db/__init__.py,sha256=sttX0mHVV0ESI1SJ1kcxUKiuEwqeP-BWsst0o_9Yo44,2810
6
- fractal_server/app/models/__init__.py,sha256=xJWiGAwpXmCpnFMC4c_HTqoUCzMOXrakoGLUH_uMvdA,415
6
+ fractal_server/app/models/__init__.py,sha256=93eFiAaiV6cU_Mf8WQDi_Ir7vqakAiS5H9Pl7qAb4JE,367
7
7
  fractal_server/app/models/linkusergroup.py,sha256=3KkkE4QIUAlTrBAZs_tVy0pGvAxUAq6yOEjflct_z2M,678
8
8
  fractal_server/app/models/linkuserproject.py,sha256=hvaxh3Lkiy2uUCwB8gvn8RorCpvxSSdzWdCS_U1GL7g,315
9
- fractal_server/app/models/security.py,sha256=VThWDEmzUP4SgLsAvd5WjJ1p2WVxBuc6D5TMQJaOyd8,3873
10
- fractal_server/app/models/user_settings.py,sha256=u0GOK1JdqDmXzA8hK2JV93rZxY_rF-0oKMkArRolnN8,1201
9
+ fractal_server/app/models/security.py,sha256=f44hOx4Tro0-KLj5N70_gv1UsFrr5ygHY7W_XHFaOJE,3546
11
10
  fractal_server/app/models/v2/__init__.py,sha256=A668GF4z_UPar6kAOwC-o_qUo3CIRJ3SmBGYTs3Xc7k,923
12
11
  fractal_server/app/models/v2/accounting.py,sha256=i-2TsjqyuclxFQ21C-TeDoss7ZBTRuXdzIJfVr2UxwE,1081
13
12
  fractal_server/app/models/v2/dataset.py,sha256=P_zy4dPQAqrCALQ6737VkAFk1SvcgYjnslGUZhPI8sc,1226
14
13
  fractal_server/app/models/v2/history.py,sha256=CBN2WVg9vW5pHU1RP8TkB_nnJrwnuifCcxgnd53UtEE,2163
15
14
  fractal_server/app/models/v2/job.py,sha256=YYzt3ef2CU1WXFNjlltR3ft2kM9T0Hq8oskSipQSxuM,2042
16
15
  fractal_server/app/models/v2/profile.py,sha256=QqOE7XGeq-ckQAbGhcgzDN5zFFaTNrtcuWgOXy9psR8,440
17
- fractal_server/app/models/v2/project.py,sha256=oXNcuNVDeNYZ60fwAx-Y_vnkS3xd9pwFdoT2pZmnBNI,918
16
+ fractal_server/app/models/v2/project.py,sha256=DJgTZG1NTA_pbLlY0Jy3WFBE8X8fBMkVALfDWK5ZuHY,832
18
17
  fractal_server/app/models/v2/resource.py,sha256=ReaBGtKb3e0_1PZOZncdGqrttkrC-bsgDCv3wPCGfOs,3512
19
18
  fractal_server/app/models/v2/task.py,sha256=iBIQB8POQE5MyKvLZhw7jZWlBhbrThzCDzRTcgiAczQ,1493
20
- fractal_server/app/models/v2/task_group.py,sha256=gHkuyIBw8hkoMCwHgo08SWVMc8T1MXC5xqoW2YNd5Sw,4753
19
+ fractal_server/app/models/v2/task_group.py,sha256=v9hI-R8mXhm0LvE_I_YG3M8VHz0V9DohhGNykdEgcB8,4667
21
20
  fractal_server/app/models/v2/workflow.py,sha256=gBjDXO-RytVT81aAlesImBhmVHrwNUrmsF_UsGa1qLM,1057
22
21
  fractal_server/app/models/v2/workflowtask.py,sha256=qkTc-hcFLpJUVsEUbnDq2BJL0qg9jagy2doZeusF1ek,1266
23
22
  fractal_server/app/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -94,15 +93,14 @@ fractal_server/app/schemas/v2/workflow.py,sha256=L-dW6SzCH_VNoH6ENip44lTgGGqVYHH
94
93
  fractal_server/app/schemas/v2/workflowtask.py,sha256=6eweAMyziwaoMT-7R1fVJYunIeZKzT0-7fAVgPO_FEc,3639
95
94
  fractal_server/app/security/__init__.py,sha256=k-La8Da89C1hSUGsiidrWo6Az4u6dbe5PzN1Ctt1t34,18394
96
95
  fractal_server/app/security/signup_email.py,sha256=kphjq6TAygvPpYpg95QJWefyqmzdVrGz7fyRMctUJWE,1982
97
- fractal_server/app/shutdown.py,sha256=ViSNJyXWU_iWPSDOOMGNh_iQdUFrdPh_jvf8vVKLpAo,1950
96
+ fractal_server/app/shutdown.py,sha256=QU4DfNvqwUXlHiLORtYJit4DxlFQo014SKTfs4dcE2U,2295
98
97
  fractal_server/config/__init__.py,sha256=ZCmroNB50sUxJiFtkW0a4fFtmfyPnL4LWhtKY5FbQfg,737
99
98
  fractal_server/config/_data.py,sha256=9Jyt83yrSsr_0_9ANWDAXz88_jjyFlcB5VWJGXq8aUY,2311
100
99
  fractal_server/config/_database.py,sha256=k1z__MrslQjmel34yFvge0sroPUs1vBtT_OSlPY8pN8,1690
101
100
  fractal_server/config/_email.py,sha256=j1QmZCyspNbD1xxkypc9Kv299tU3vTO1AqDFJ8-LZzQ,4201
102
- fractal_server/config/_main.py,sha256=9v64gJsvY1oGP70_AoJMnyMIeRo7FcIg6T8NDV-p9as,1992
101
+ fractal_server/config/_main.py,sha256=NmpNuNezVI7MpuAiZ9AOlo3Fc-4x73FIaFm-UMhppEw,1996
103
102
  fractal_server/config/_oauth.py,sha256=7J4FphGVFfVmtQycCkas6scEJQJGZUGEzQ-t2PZiqSo,1934
104
103
  fractal_server/config/_settings_config.py,sha256=tsyXQOnn9QKCFJD6hRo_dJXlQQyl70DbqgHMJoZ1xnY,144
105
- fractal_server/data_migrations/2_17_0.py,sha256=DGxrQ5JSiJW5tzpfydCjYKjX7FsiF3y_k34b0mo_RaY,11757
106
104
  fractal_server/data_migrations/README.md,sha256=_3AEFvDg9YkybDqCLlFPdDmGJvr6Tw7HRI14aZ3LOIw,398
107
105
  fractal_server/data_migrations/tools.py,sha256=LeMeASwYGtEqd-3wOLle6WARdTGAimoyMmRbbJl-hAM,572
108
106
  fractal_server/exceptions.py,sha256=7ftpWwNsTQmNonWCynhH5ErUh1haPPhIaVPrNHla7-o,53
@@ -122,7 +120,9 @@ fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py,sha
122
120
  fractal_server/migrations/versions/1a83a5260664_rename.py,sha256=EkzTAjbJm7CfsLraIUbH9hkTj4M6XvmziVb4K9ZjKmQ,790
123
121
  fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py,sha256=7OW3HmqAePHx53OWdEPzNxvtupxSR0lB_6tZF1b3JIM,1604
124
122
  fractal_server/migrations/versions/316140ff7ee1_remove_usersettings_cache_dir.py,sha256=lANgTox0rz459_yo1Rw7fGCT1qw5sUCUXTLUMc_Bzf8,911
123
+ fractal_server/migrations/versions/45fbb391d7af_make_resource_id_fk_non_nullable.py,sha256=y9zr161YIWgnWbaMg1rahKN4b-vHjT3f5VSeoOAHaqI,1296
125
124
  fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py,sha256=vePkVm1iUHiPNKLQ3KR7BBLdHruqBdl87j_tUCbMbEA,1414
125
+ fractal_server/migrations/versions/49d0856e9569_drop_table.py,sha256=qoq7cGUQmrnUj_wpV2mRqVneyoKqglgbrgzW_8eS_5w,1835
126
126
  fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py,sha256=-wHe-fOffmYeAm0JXVl_lxZ7hhDkaEVqxgxpHkb_uL8,954
127
127
  fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py,sha256=Mob8McGYAcmgvrseyyYOa54E6Gsgr-4SiGdC-r9O4_A,1157
128
128
  fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py,sha256=JOrVa6mGzqZ6H61YCFVOed64vFRjTWGWyN3z7NE3T08,3270
@@ -130,6 +130,7 @@ fractal_server/migrations/versions/50a13d6138fd_initial_schema.py,sha256=zwXegXs
130
130
  fractal_server/migrations/versions/5bf02391cfef_v2.py,sha256=axhNkr_H6R4rRbY7oGYazNbFvPXeSyBDWFVbKNmiqs8,8433
131
131
  fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py,sha256=Q-DsMzG3IcUV2Ol1dhJWosDvKERamBE6QvA2zzS5zpQ,1632
132
132
  fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py,sha256=mbWuCkTpRAdGbRhW7lhXs_e5S6O37UAcCN6JfoY5H8A,1353
133
+ fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py,sha256=LAC1Uv4SeLkqjXPyqj5Mof8L0105gxqS1TYKzNVX4GE,795
133
134
  fractal_server/migrations/versions/791ce783d3d8_add_indices.py,sha256=gNE6AgJgeJZY99Fbd336Z9see3gRMQvuNBC0xDk_5sw,1154
134
135
  fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py,sha256=U7t_8n58taRkd9sxCXOshrTr9M5AhlsQne8SGKa5Jt4,6377
135
136
  fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py,sha256=NSCuhANChsg76vBkShBl-9tQ4VEHubOjtAv1etHhlvY,2684
@@ -149,6 +150,7 @@ fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py,sha25
149
150
  fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py,sha256=loDrqBB-9U3vqLKePEeJy4gK4EuPs_1F345mdrnoCt0,1293
150
151
  fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py,sha256=Q01lPlBNQgi3hpoUquWj2QUEF7cTsyQ7uikUhWunzWY,10035
151
152
  fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py,sha256=Y1cPwmFOZ4mx3v2XZM6adgu8u0L0VD_R4ADURyMb2ro,1102
153
+ fractal_server/migrations/versions/caba9fb1ea5e_drop_useroauth_user_settings_id.py,sha256=8tlWVmux-c-fB9hMO4JEsaPMXRwLN_X3PpC0rUuFrYw,1320
152
154
  fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py,sha256=HN3_Pk8G81SzdYjg4K1RZAyjKSlsZGvcYE2nWOUbwxQ,3861
153
155
  fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py,sha256=6cHEZFuTXiQg9yu32Y3RH1XAl71av141WQ6UMbiITIg,949
154
156
  fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py,sha256=yGWSA2HIHUybcVy66xBITk08opV2DFYSCIIrulaUZhI,901
@@ -255,8 +257,8 @@ fractal_server/types/validators/_workflow_task_arguments_validators.py,sha256=HL
255
257
  fractal_server/urls.py,sha256=QjIKAC1a46bCdiPMu3AlpgFbcv6a4l3ABcd5xz190Og,471
256
258
  fractal_server/utils.py,sha256=SYVVUuXe_nWyrJLsy7QA-KJscwc5PHEXjvsW4TK7XQI,2180
257
259
  fractal_server/zip_tools.py,sha256=H0w7wS5yE4ebj7hw1_77YQ959dl2c-L0WX6J_ro1TY4,4884
258
- fractal_server-2.17.1a0.dist-info/METADATA,sha256=XJQY23pLEp0LL8hg0EN9kPh7P6wfJLD9GwcHq8psXQo,4226
259
- fractal_server-2.17.1a0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
260
- fractal_server-2.17.1a0.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
261
- fractal_server-2.17.1a0.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
262
- fractal_server-2.17.1a0.dist-info/RECORD,,
260
+ fractal_server-2.17.1a1.dist-info/METADATA,sha256=PTysEV2k6eEa799QPMn2C9pTxTL2jR0wn1luvcrCCPM,4226
261
+ fractal_server-2.17.1a1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
262
+ fractal_server-2.17.1a1.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
263
+ fractal_server-2.17.1a1.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
264
+ fractal_server-2.17.1a1.dist-info/RECORD,,
@@ -1,37 +0,0 @@
1
- from sqlalchemy import Column
2
- from sqlalchemy.dialects.postgresql import JSONB
3
- from sqlmodel import Field
4
- from sqlmodel import SQLModel
5
-
6
-
7
- # TODO-2.17.1: Drop `UserSettings`
8
- class UserSettings(SQLModel, table=True):
9
- """
10
- Comprehensive list of user settings.
11
-
12
- Attributes:
13
- id: ID of database object
14
- slurm_accounts:
15
- List of SLURM accounts, to be used upon Fractal job submission.
16
- ssh_host: SSH-reachable host where a SLURM client is available.
17
- ssh_username: User on `ssh_host`.
18
- ssh_private_key_path: Path of private SSH key for `ssh_username`.
19
- slurm_user: Local user, to be impersonated via `sudo -u`
20
- project_dir: Folder where `slurm_user` can write.
21
- """
22
-
23
- __tablename__ = "user_settings"
24
-
25
- id: int | None = Field(default=None, primary_key=True)
26
- slurm_accounts: list[str] = Field(
27
- sa_column=Column(JSONB, server_default="[]", nullable=False)
28
- )
29
- ssh_host: str | None = None
30
- ssh_username: str | None = None
31
- ssh_private_key_path: str | None = None
32
-
33
- slurm_user: str | None = None
34
- project_dir: str | None = None
35
-
36
- ssh_tasks_dir: str | None = None
37
- ssh_jobs_dir: str | None = None
@@ -1,339 +0,0 @@
1
- import json
2
- import logging
3
- import sys
4
- from pathlib import Path
5
- from typing import Any
6
-
7
- from dotenv.main import DotEnv
8
- from pydantic import BaseModel
9
- from sqlalchemy.orm import Session
10
- from sqlalchemy.sql.operators import is_
11
- from sqlalchemy.sql.operators import is_not
12
- from sqlmodel import select
13
-
14
- from fractal_server.app.db import get_sync_db
15
- from fractal_server.app.models import Profile
16
- from fractal_server.app.models import ProjectV2
17
- from fractal_server.app.models import Resource
18
- from fractal_server.app.models import TaskGroupV2
19
- from fractal_server.app.models import UserOAuth
20
- from fractal_server.app.models import UserSettings
21
- from fractal_server.app.schemas.v2.profile import cast_serialize_profile
22
- from fractal_server.app.schemas.v2.resource import cast_serialize_resource
23
- from fractal_server.config import get_settings
24
- from fractal_server.runner.config import JobRunnerConfigLocal
25
- from fractal_server.runner.config import JobRunnerConfigSLURM
26
- from fractal_server.tasks.config import TasksPixiSettings
27
- from fractal_server.tasks.config import TasksPythonSettings
28
- from fractal_server.types import AbsolutePathStr
29
- from fractal_server.types import ListUniqueNonEmptyString
30
- from fractal_server.urls import normalize_url
31
-
32
- logging.basicConfig(level=logging.INFO)
33
-
34
-
35
- class UserUpdateInfo(BaseModel):
36
- user_id: int
37
- project_dir: AbsolutePathStr
38
- slurm_accounts: ListUniqueNonEmptyString
39
-
40
-
41
- class ProfileUsersUpdateInfo(BaseModel):
42
- data: dict[str, Any]
43
- user_updates: list[UserUpdateInfo]
44
-
45
-
46
- def _get_user_settings(user: UserOAuth, db: Session) -> UserSettings:
47
- if user.user_settings_id is None:
48
- sys.exit(f"User {user.email} is active but {user.user_settings_id=}.")
49
- user_settings = db.get(UserSettings, user.user_settings_id)
50
- return user_settings
51
-
52
-
53
- def assert_user_setting_key(
54
- user: UserOAuth,
55
- user_settings: UserSettings,
56
- keys: list[str],
57
- ) -> None:
58
- for key in keys:
59
- if getattr(user_settings, key) is None:
60
- sys.exit(
61
- f"User {user.email} is active and verified but their "
62
- f"user settings have {key}=None."
63
- )
64
-
65
-
66
- def prepare_profile_and_user_updates() -> dict[str, ProfileUsersUpdateInfo]:
67
- settings = get_settings()
68
- profiles_and_users: dict[str, ProfileUsersUpdateInfo] = {}
69
- with next(get_sync_db()) as db:
70
- # Get active&verified users
71
- res = db.execute(
72
- select(UserOAuth)
73
- .where(is_(UserOAuth.is_active, True))
74
- .where(is_(UserOAuth.is_verified, True))
75
- .order_by(UserOAuth.id)
76
- )
77
- for user in res.unique().scalars().all():
78
- # Get user settings
79
- user_settings = _get_user_settings(user=user, db=db)
80
- assert_user_setting_key(user, user_settings, ["project_dir"])
81
-
82
- # Prepare profile data and user update
83
- new_profile_data = dict()
84
- if settings.FRACTAL_RUNNER_BACKEND == "local":
85
- username = None
86
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
87
- assert_user_setting_key(user, user_settings, ["slurm_user"])
88
- username = user_settings.slurm_user
89
- elif settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
90
- assert_user_setting_key(
91
- user,
92
- user_settings,
93
- [
94
- "ssh_username",
95
- "ssh_private_key_path",
96
- "ssh_tasks_dir",
97
- "ssh_jobs_dir",
98
- ],
99
- )
100
- username = user_settings.ssh_username
101
- new_profile_data.update(
102
- ssh_key_path=user_settings.ssh_private_key_path,
103
- tasks_remote_dir=normalize_url(
104
- user_settings.ssh_tasks_dir
105
- ),
106
- jobs_remote_dir=normalize_url(user_settings.ssh_jobs_dir),
107
- )
108
-
109
- new_profile_data.update(
110
- name=f"Profile {username}",
111
- username=username,
112
- resource_type=settings.FRACTAL_RUNNER_BACKEND,
113
- )
114
- cast_serialize_profile(new_profile_data)
115
-
116
- user_update_info = UserUpdateInfo(
117
- user_id=user.id,
118
- project_dir=normalize_url(user_settings.project_dir),
119
- slurm_accounts=user_settings.slurm_accounts or [],
120
- )
121
-
122
- if username in profiles_and_users.keys():
123
- if profiles_and_users[username].data != new_profile_data:
124
- error_msg = (
125
- "Profile data mismatch.\n"
126
- f"{profiles_and_users[username].data=}\n"
127
- f"{new_profile_data=}"
128
- )
129
- logging.error(error_msg)
130
- sys.exit(error_msg)
131
- profiles_and_users[username].user_updates.append(
132
- user_update_info
133
- )
134
- else:
135
- profiles_and_users[username] = ProfileUsersUpdateInfo(
136
- data=new_profile_data,
137
- user_updates=[user_update_info],
138
- )
139
-
140
- return profiles_and_users
141
-
142
-
143
- def get_old_dotenv_variables() -> dict[str, str | None]:
144
- """
145
- See
146
- https://github.com/fractal-analytics-platform/fractal-server/blob/2.16.x/fractal_server/config.py
147
- """
148
- OLD_DOTENV_FILE = ".fractal_server.env.old"
149
- return dict(
150
- **DotEnv(
151
- dotenv_path=OLD_DOTENV_FILE,
152
- override=False,
153
- ).dict()
154
- )
155
-
156
-
157
- def get_TasksPythonSettings(
158
- old_config: dict[str, str | None]
159
- ) -> dict[str, Any]:
160
- versions = {}
161
- for version_underscore in ["3_9", "3_10", "3_11", "3_12"]:
162
- key = f"FRACTAL_TASKS_PYTHON_{version_underscore}"
163
- version_dot = version_underscore.replace("_", ".")
164
- value = old_config.get(key, None)
165
- if value is not None:
166
- versions[version_dot] = value
167
- obj = TasksPythonSettings(
168
- default_version=old_config["FRACTAL_TASKS_PYTHON_DEFAULT_VERSION"],
169
- versions=versions,
170
- pip_cache_dir=old_config.get("FRACTAL_PIP_CACHE_DIR", None),
171
- )
172
- return obj.model_dump()
173
-
174
-
175
- def get_TasksPixiSettings(old_config: dict[str, str | None]) -> dict[str, Any]:
176
- pixi_file = old_config.get("FRACTAL_PIXI_CONFIG_FILE", None)
177
- if pixi_file is None:
178
- return {}
179
- with open(pixi_file) as f:
180
- old_pixi_config = json.load(f)
181
- TasksPixiSettings(**old_pixi_config)
182
- return old_pixi_config
183
-
184
-
185
- def get_JobRunnerConfigSLURM(
186
- old_config: dict[str, str | None]
187
- ) -> dict[str, Any]:
188
- slurm_file = old_config["FRACTAL_SLURM_CONFIG_FILE"]
189
- with open(slurm_file) as f:
190
- old_slurm_config = json.load(f)
191
- JobRunnerConfigSLURM(**old_slurm_config)
192
- return old_slurm_config
193
-
194
-
195
- def get_JobRunnerConfigLocal(
196
- old_config: dict[str, str | None]
197
- ) -> dict[str, Any]:
198
- local_file = old_config.get("FRACTAL_LOCAL_CONFIG_FILE", None)
199
- if local_file is None or not Path(local_file).exists():
200
- return JobRunnerConfigLocal().model_dump()
201
- else:
202
- with open(local_file) as f:
203
- old_local_config = json.load(f)
204
- JobRunnerConfigLocal(**old_local_config)
205
- return old_local_config
206
-
207
-
208
- def get_ssh_host() -> str:
209
- with next(get_sync_db()) as db:
210
- res = db.execute(
211
- select(UserSettings.ssh_host).where(
212
- is_not(UserSettings.ssh_host, None)
213
- )
214
- )
215
- hosts = res.scalars().all()
216
- if len(set(hosts)) > 1:
217
- host = max(set(hosts), key=hosts.count)
218
- print(f"MOST FREQUENT HOST: {host}")
219
- else:
220
- host = hosts[0]
221
- return host
222
-
223
-
224
- def prepare_resource_data(old_config: dict[str, str | None]) -> dict[str, Any]:
225
- settings = get_settings()
226
-
227
- resource_data = dict(
228
- type=settings.FRACTAL_RUNNER_BACKEND,
229
- name="Resource Name",
230
- tasks_python_config=get_TasksPythonSettings(old_config),
231
- tasks_pixi_config=get_TasksPixiSettings(old_config),
232
- tasks_local_dir=old_config["FRACTAL_TASKS_DIR"],
233
- jobs_local_dir=old_config["FRACTAL_RUNNER_WORKING_BASE_DIR"],
234
- jobs_poll_interval=int(
235
- old_config.get("FRACTAL_SLURM_POLL_INTERVAL", 15)
236
- ),
237
- )
238
- if settings.FRACTAL_RUNNER_BACKEND == "local":
239
- resource_data["jobs_runner_config"] = get_JobRunnerConfigLocal(
240
- old_config
241
- )
242
- elif settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
243
- resource_data["jobs_slurm_python_worker"] = old_config[
244
- "FRACTAL_SLURM_WORKER_PYTHON"
245
- ]
246
- resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
247
- old_config
248
- )
249
- else:
250
- resource_data["jobs_slurm_python_worker"] = old_config[
251
- "FRACTAL_SLURM_WORKER_PYTHON"
252
- ]
253
- resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
254
- old_config
255
- )
256
- resource_data["host"] = get_ssh_host()
257
-
258
- resource_data = cast_serialize_resource(resource_data)
259
-
260
- return resource_data
261
-
262
-
263
- def fix_db():
264
- logging.info("START preliminary checks.")
265
-
266
- # Read old env file
267
- old_config = get_old_dotenv_variables()
268
-
269
- # Prepare resource data
270
- logging.info("START prepare_resource_data")
271
- resource_data = prepare_resource_data(old_config)
272
- logging.info("END prepare_resource_data")
273
-
274
- # Prepare profile/users data
275
- logging.info("START prepare_profile_and_user_updates")
276
- profile_and_user_updates = prepare_profile_and_user_updates()
277
- logging.info("END prepare_profile_and_user_updates")
278
-
279
- logging.info("END preliminary checks.")
280
- print()
281
-
282
- with next(get_sync_db()) as db:
283
- # Create new resource
284
- resource = Resource(**resource_data)
285
- db.add(resource)
286
- db.commit()
287
- db.refresh(resource)
288
- db.expunge(resource)
289
- resource_id = resource.id
290
- logging.info(f"Created resource with {resource_id=}.")
291
-
292
- # Update task groups
293
- res = db.execute(select(TaskGroupV2).order_by(TaskGroupV2.id))
294
- for taskgroup in res.scalars().all():
295
- taskgroup.resource_id = resource_id
296
- db.add(taskgroup)
297
- db.commit()
298
- logging.info(f"Set {resource_id=} foreign key for all task groups.")
299
-
300
- # Update projects
301
- res = db.execute(select(ProjectV2).order_by(ProjectV2.id))
302
- for project in res.scalars().all():
303
- project.resource_id = resource_id
304
- db.add(project)
305
- db.commit()
306
- logging.info(f"Set {resource_id=} foreign key for all projects.")
307
- print()
308
-
309
- db.expunge_all()
310
-
311
- for _, info in profile_and_user_updates.items():
312
- # Create profile
313
- profile_data = info.data
314
- profile_data["resource_id"] = resource_id
315
- profile = Profile(**profile_data)
316
- db.add(profile)
317
- db.commit()
318
- db.refresh(profile)
319
- db.expunge(profile)
320
- profile_id = profile.id
321
- logging.info(
322
- f"Created profile '{profile.name}', with {profile.id=}."
323
- )
324
-
325
- # Update users
326
- for user_update in info.user_updates:
327
- user = db.get(UserOAuth, user_update.user_id)
328
- user.profile_id = profile_id
329
- user.project_dir = user_update.project_dir
330
- user.slurm_accounts = user_update.slurm_accounts
331
- db.add(user)
332
- logging.info(f"Updated {user.email} with {user.project_dir=}.")
333
- logging.info(
334
- f"Associated {user.email} to profile {profile.name}."
335
- )
336
- print()
337
- db.commit()
338
-
339
- logging.info("END - all ok.")