fractal-server 2.14.5__py3-none-any.whl → 2.14.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -2
  3. fractal_server/app/models/security.py +8 -8
  4. fractal_server/app/models/user_settings.py +8 -10
  5. fractal_server/app/models/v2/accounting.py +2 -3
  6. fractal_server/app/models/v2/dataset.py +1 -2
  7. fractal_server/app/models/v2/history.py +3 -4
  8. fractal_server/app/models/v2/job.py +10 -11
  9. fractal_server/app/models/v2/project.py +1 -2
  10. fractal_server/app/models/v2/task.py +13 -14
  11. fractal_server/app/models/v2/task_group.py +15 -16
  12. fractal_server/app/models/v2/workflow.py +1 -2
  13. fractal_server/app/models/v2/workflowtask.py +6 -7
  14. fractal_server/app/routes/admin/v2/accounting.py +3 -4
  15. fractal_server/app/routes/admin/v2/job.py +13 -14
  16. fractal_server/app/routes/admin/v2/project.py +2 -4
  17. fractal_server/app/routes/admin/v2/task.py +11 -13
  18. fractal_server/app/routes/admin/v2/task_group.py +15 -17
  19. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +5 -8
  20. fractal_server/app/routes/api/v2/__init__.py +2 -0
  21. fractal_server/app/routes/api/v2/_aux_functions.py +7 -9
  22. fractal_server/app/routes/api/v2/_aux_functions_history.py +1 -1
  23. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +1 -3
  24. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +5 -6
  25. fractal_server/app/routes/api/v2/dataset.py +6 -8
  26. fractal_server/app/routes/api/v2/history.py +5 -8
  27. fractal_server/app/routes/api/v2/images.py +2 -3
  28. fractal_server/app/routes/api/v2/job.py +5 -6
  29. fractal_server/app/routes/api/v2/pre_submission_checks.py +1 -3
  30. fractal_server/app/routes/api/v2/project.py +2 -4
  31. fractal_server/app/routes/api/v2/status_legacy.py +2 -4
  32. fractal_server/app/routes/api/v2/submit.py +3 -4
  33. fractal_server/app/routes/api/v2/task.py +6 -7
  34. fractal_server/app/routes/api/v2/task_collection.py +11 -13
  35. fractal_server/app/routes/api/v2/task_collection_custom.py +4 -4
  36. fractal_server/app/routes/api/v2/task_group.py +6 -8
  37. fractal_server/app/routes/api/v2/task_group_lifecycle.py +6 -9
  38. fractal_server/app/routes/api/v2/task_version_update.py +270 -0
  39. fractal_server/app/routes/api/v2/workflow.py +5 -6
  40. fractal_server/app/routes/api/v2/workflow_import.py +3 -5
  41. fractal_server/app/routes/api/v2/workflowtask.py +2 -114
  42. fractal_server/app/routes/auth/current_user.py +2 -2
  43. fractal_server/app/routes/pagination.py +2 -3
  44. fractal_server/app/runner/exceptions.py +15 -16
  45. fractal_server/app/runner/executors/base_runner.py +3 -3
  46. fractal_server/app/runner/executors/call_command_wrapper.py +1 -1
  47. fractal_server/app/runner/executors/local/get_local_config.py +2 -3
  48. fractal_server/app/runner/executors/local/runner.py +1 -1
  49. fractal_server/app/runner/executors/slurm_common/_batching.py +2 -3
  50. fractal_server/app/runner/executors/slurm_common/_slurm_config.py +27 -29
  51. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +32 -14
  52. fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +2 -3
  53. fractal_server/app/runner/executors/slurm_common/remote.py +2 -2
  54. fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +2 -3
  55. fractal_server/app/runner/executors/slurm_ssh/run_subprocess.py +2 -3
  56. fractal_server/app/runner/executors/slurm_ssh/runner.py +5 -4
  57. fractal_server/app/runner/executors/slurm_sudo/_subprocess_run_as_user.py +1 -2
  58. fractal_server/app/runner/executors/slurm_sudo/runner.py +7 -8
  59. fractal_server/app/runner/set_start_and_last_task_index.py +2 -5
  60. fractal_server/app/runner/shutdown.py +5 -11
  61. fractal_server/app/runner/task_files.py +3 -5
  62. fractal_server/app/runner/v2/_local.py +3 -4
  63. fractal_server/app/runner/v2/_slurm_ssh.py +8 -7
  64. fractal_server/app/runner/v2/_slurm_sudo.py +8 -9
  65. fractal_server/app/runner/v2/runner.py +4 -5
  66. fractal_server/app/runner/v2/runner_functions.py +4 -5
  67. fractal_server/app/runner/v2/submit_workflow.py +12 -11
  68. fractal_server/app/runner/v2/task_interface.py +2 -3
  69. fractal_server/app/runner/versions.py +1 -2
  70. fractal_server/app/schemas/user.py +2 -4
  71. fractal_server/app/schemas/user_group.py +1 -2
  72. fractal_server/app/schemas/user_settings.py +19 -21
  73. fractal_server/app/schemas/v2/dataset.py +2 -3
  74. fractal_server/app/schemas/v2/dumps.py +13 -15
  75. fractal_server/app/schemas/v2/history.py +6 -7
  76. fractal_server/app/schemas/v2/job.py +17 -18
  77. fractal_server/app/schemas/v2/manifest.py +12 -13
  78. fractal_server/app/schemas/v2/status_legacy.py +2 -2
  79. fractal_server/app/schemas/v2/task.py +29 -30
  80. fractal_server/app/schemas/v2/task_collection.py +8 -9
  81. fractal_server/app/schemas/v2/task_group.py +22 -23
  82. fractal_server/app/schemas/v2/workflow.py +1 -2
  83. fractal_server/app/schemas/v2/workflowtask.py +27 -29
  84. fractal_server/app/security/__init__.py +10 -12
  85. fractal_server/config.py +32 -33
  86. fractal_server/images/models.py +2 -4
  87. fractal_server/images/tools.py +4 -7
  88. fractal_server/logger.py +3 -5
  89. fractal_server/ssh/_fabric.py +37 -12
  90. fractal_server/string_tools.py +2 -2
  91. fractal_server/syringe.py +1 -1
  92. fractal_server/tasks/v2/local/collect.py +2 -3
  93. fractal_server/tasks/v2/local/deactivate.py +1 -1
  94. fractal_server/tasks/v2/local/reactivate.py +1 -1
  95. fractal_server/tasks/v2/ssh/collect.py +256 -245
  96. fractal_server/tasks/v2/ssh/deactivate.py +210 -187
  97. fractal_server/tasks/v2/ssh/reactivate.py +154 -146
  98. fractal_server/tasks/v2/utils_background.py +2 -3
  99. fractal_server/types/__init__.py +1 -2
  100. fractal_server/types/validators/_filter_validators.py +1 -2
  101. fractal_server/utils.py +4 -5
  102. fractal_server/zip_tools.py +1 -1
  103. {fractal_server-2.14.5.dist-info → fractal_server-2.14.7.dist-info}/METADATA +2 -3
  104. {fractal_server-2.14.5.dist-info → fractal_server-2.14.7.dist-info}/RECORD +107 -107
  105. fractal_server/app/history/__init__.py +0 -0
  106. {fractal_server-2.14.5.dist-info → fractal_server-2.14.7.dist-info}/LICENSE +0 -0
  107. {fractal_server-2.14.5.dist-info → fractal_server-2.14.7.dist-info}/WHEEL +0 -0
  108. {fractal_server-2.14.5.dist-info → fractal_server-2.14.7.dist-info}/entry_points.txt +0 -0
@@ -14,7 +14,8 @@ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
14
14
  from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
15
15
  from fractal_server.logger import reset_logger_handlers
16
16
  from fractal_server.logger import set_logger
17
- from fractal_server.ssh._fabric import FractalSSH
17
+ from fractal_server.ssh._fabric import SingleUseFractalSSH
18
+ from fractal_server.ssh._fabric import SSHConfig
18
19
  from fractal_server.tasks.utils import get_log_path
19
20
  from fractal_server.tasks.v2.utils_background import get_current_log
20
21
  from fractal_server.tasks.v2.utils_python_interpreter import (
@@ -28,7 +29,7 @@ def reactivate_ssh(
28
29
  *,
29
30
  task_group_activity_id: int,
30
31
  task_group_id: int,
31
- fractal_ssh: FractalSSH,
32
+ ssh_config: SSHConfig,
32
33
  tasks_base_dir: str,
33
34
  ) -> None:
34
35
  """
@@ -40,7 +41,7 @@ def reactivate_ssh(
40
41
  Arguments:
41
42
  task_group_id:
42
43
  task_group_activity_id:
43
- fractal_ssh:
44
+ ssh_config:
44
45
  tasks_base_dir:
45
46
  Only used as a `safe_root` in `remove_dir`, and typically set to
46
47
  `user_settings.ssh_tasks_dir`.
@@ -55,150 +56,157 @@ def reactivate_ssh(
55
56
  log_file_path=log_file_path,
56
57
  )
57
58
 
58
- with next(get_sync_db()) as db:
59
-
60
- # Get main objects from db
61
- activity = db.get(TaskGroupActivityV2, task_group_activity_id)
62
- task_group = db.get(TaskGroupV2, task_group_id)
63
- if activity is None or task_group is None:
64
- # Use `logging` directly
65
- logging.error(
66
- "Cannot find database rows with "
67
- f"{task_group_id=} and {task_group_activity_id=}:\n"
68
- f"{task_group=}\n{activity=}. Exit."
69
- )
70
- return
71
-
72
- # Log some info
73
- logger.info("START")
74
- for key, value in task_group.model_dump().items():
75
- logger.debug(f"task_group.{key}: {value}")
76
-
77
- # Check that SSH connection works
78
- try:
79
- fractal_ssh.check_connection()
80
- except Exception as e:
81
- logger.error("Cannot establish SSH connection.")
82
- fail_and_cleanup(
83
- task_group=task_group,
84
- task_group_activity=activity,
85
- logger_name=LOGGER_NAME,
86
- log_file_path=log_file_path,
87
- exception=e,
88
- db=db,
89
- )
90
- return
91
-
92
- # Check that the (remote) task_group venv_path does not exist
93
- if fractal_ssh.remote_exists(task_group.venv_path):
94
- error_msg = f"{task_group.venv_path} already exists."
95
- logger.error(error_msg)
96
- fail_and_cleanup(
97
- task_group=task_group,
98
- task_group_activity=activity,
99
- logger_name=LOGGER_NAME,
100
- log_file_path=log_file_path,
101
- exception=FileExistsError(error_msg),
102
- db=db,
103
- )
104
- return
105
-
106
- try:
107
- activity.status = TaskGroupActivityStatusV2.ONGOING
108
- activity = add_commit_refresh(obj=activity, db=db)
109
-
110
- # Prepare replacements for templates
111
- replacements = get_collection_replacements(
112
- task_group=task_group,
113
- python_bin=get_python_interpreter_v2(
114
- python_version=task_group.python_version
115
- ),
116
- )
117
-
118
- # Prepare replacements for templates
119
- pip_freeze_file_local = f"{tmpdir}/pip_freeze.txt"
120
- pip_freeze_file_remote = (
121
- Path(task_group.path) / "_tmp_pip_freeze.txt"
122
- ).as_posix()
123
- with open(pip_freeze_file_local, "w") as f:
124
- f.write(task_group.pip_freeze)
125
- fractal_ssh.send_file(
126
- local=pip_freeze_file_local, remote=pip_freeze_file_remote
127
- )
128
- replacements.append(
129
- ("__PIP_FREEZE_FILE__", pip_freeze_file_remote)
130
- )
131
-
132
- # Define script_dir_remote and create it if missing
133
- script_dir_remote = (
134
- Path(task_group.path) / SCRIPTS_SUBFOLDER
135
- ).as_posix()
136
- fractal_ssh.mkdir(folder=script_dir_remote, parents=True)
137
-
138
- # Prepare common arguments for `_customize_and_run_template`
139
- common_args = dict(
140
- replacements=replacements,
141
- script_dir_local=(
142
- Path(tmpdir) / SCRIPTS_SUBFOLDER
143
- ).as_posix(),
144
- script_dir_remote=script_dir_remote,
145
- prefix=(
146
- f"{int(time.time())}_"
147
- f"{TaskGroupActivityActionV2.REACTIVATE.value}"
148
- ),
149
- fractal_ssh=fractal_ssh,
150
- logger_name=LOGGER_NAME,
151
- )
152
-
153
- # Create remote directory for scripts
154
- fractal_ssh.mkdir(folder=script_dir_remote)
155
-
156
- logger.info("start - create venv")
157
- _customize_and_run_template(
158
- template_filename="1_create_venv.sh",
159
- **common_args,
160
- )
161
- logger.info("end - create venv")
162
- activity.log = get_current_log(log_file_path)
163
- activity = add_commit_refresh(obj=activity, db=db)
164
-
165
- logger.info("start - install from pip freeze")
166
- _customize_and_run_template(
167
- template_filename="6_pip_install_from_freeze.sh",
168
- **common_args,
169
- )
170
- logger.info("end - install from pip freeze")
171
- activity.log = get_current_log(log_file_path)
172
- activity.status = TaskGroupActivityStatusV2.OK
173
- activity.timestamp_ended = get_timestamp()
174
- activity = add_commit_refresh(obj=activity, db=db)
175
- task_group.active = True
176
- task_group = add_commit_refresh(obj=task_group, db=db)
177
- logger.info("END")
178
-
179
- reset_logger_handlers(logger)
180
-
181
- except Exception as reactivate_e:
182
- # Delete corrupted venv_path
59
+ with SingleUseFractalSSH(
60
+ ssh_config=ssh_config,
61
+ logger_name=LOGGER_NAME,
62
+ ) as fractal_ssh:
63
+
64
+ with next(get_sync_db()) as db:
65
+
66
+ # Get main objects from db
67
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
68
+ task_group = db.get(TaskGroupV2, task_group_id)
69
+ if activity is None or task_group is None:
70
+ # Use `logging` directly
71
+ logging.error(
72
+ "Cannot find database rows with "
73
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
74
+ f"{task_group=}\n{activity=}. Exit."
75
+ )
76
+ return
77
+
78
+ # Log some info
79
+ logger.info("START")
80
+ for key, value in task_group.model_dump().items():
81
+ logger.debug(f"task_group.{key}: {value}")
82
+
83
+ # Check that SSH connection works
84
+ try:
85
+ fractal_ssh.check_connection()
86
+ except Exception as e:
87
+ logger.error("Cannot establish SSH connection.")
88
+ fail_and_cleanup(
89
+ task_group=task_group,
90
+ task_group_activity=activity,
91
+ logger_name=LOGGER_NAME,
92
+ log_file_path=log_file_path,
93
+ exception=e,
94
+ db=db,
95
+ )
96
+ return
97
+
98
+ # Check that the (remote) task_group venv_path does not exist
99
+ if fractal_ssh.remote_exists(task_group.venv_path):
100
+ error_msg = f"{task_group.venv_path} already exists."
101
+ logger.error(error_msg)
102
+ fail_and_cleanup(
103
+ task_group=task_group,
104
+ task_group_activity=activity,
105
+ logger_name=LOGGER_NAME,
106
+ log_file_path=log_file_path,
107
+ exception=FileExistsError(error_msg),
108
+ db=db,
109
+ )
110
+ return
111
+
183
112
  try:
184
- logger.info(f"Now delete folder {task_group.venv_path}")
185
- fractal_ssh.remove_folder(
186
- folder=task_group.venv_path,
187
- safe_root=tasks_base_dir,
113
+ activity.status = TaskGroupActivityStatusV2.ONGOING
114
+ activity = add_commit_refresh(obj=activity, db=db)
115
+
116
+ # Prepare replacements for templates
117
+ replacements = get_collection_replacements(
118
+ task_group=task_group,
119
+ python_bin=get_python_interpreter_v2(
120
+ python_version=task_group.python_version
121
+ ),
188
122
  )
189
- logger.info(f"Deleted folder {task_group.venv_path}")
190
- except Exception as rm_e:
191
- logger.error(
192
- "Removing folder failed.\n"
193
- f"Original error:\n{str(rm_e)}"
123
+
124
+ # Prepare replacements for templates
125
+ pip_freeze_file_local = f"{tmpdir}/pip_freeze.txt"
126
+ pip_freeze_file_remote = (
127
+ Path(task_group.path) / "_tmp_pip_freeze.txt"
128
+ ).as_posix()
129
+ with open(pip_freeze_file_local, "w") as f:
130
+ f.write(task_group.pip_freeze)
131
+ fractal_ssh.send_file(
132
+ local=pip_freeze_file_local,
133
+ remote=pip_freeze_file_remote,
134
+ )
135
+ replacements.append(
136
+ ("__PIP_FREEZE_FILE__", pip_freeze_file_remote)
137
+ )
138
+
139
+ # Define script_dir_remote and create it if missing
140
+ script_dir_remote = (
141
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
142
+ ).as_posix()
143
+ fractal_ssh.mkdir(folder=script_dir_remote, parents=True)
144
+
145
+ # Prepare common arguments for _customize_and_run_template
146
+ common_args = dict(
147
+ replacements=replacements,
148
+ script_dir_local=(
149
+ Path(tmpdir) / SCRIPTS_SUBFOLDER
150
+ ).as_posix(),
151
+ script_dir_remote=script_dir_remote,
152
+ prefix=(
153
+ f"{int(time.time())}_"
154
+ f"{TaskGroupActivityActionV2.REACTIVATE}"
155
+ ),
156
+ fractal_ssh=fractal_ssh,
157
+ logger_name=LOGGER_NAME,
194
158
  )
195
159
 
196
- fail_and_cleanup(
197
- task_group=task_group,
198
- task_group_activity=activity,
199
- logger_name=LOGGER_NAME,
200
- log_file_path=log_file_path,
201
- exception=reactivate_e,
202
- db=db,
203
- )
204
- return
160
+ # Create remote directory for scripts
161
+ fractal_ssh.mkdir(folder=script_dir_remote)
162
+
163
+ logger.info("start - create venv")
164
+ _customize_and_run_template(
165
+ template_filename="1_create_venv.sh",
166
+ **common_args,
167
+ )
168
+ logger.info("end - create venv")
169
+ activity.log = get_current_log(log_file_path)
170
+ activity = add_commit_refresh(obj=activity, db=db)
171
+
172
+ logger.info("start - install from pip freeze")
173
+ _customize_and_run_template(
174
+ template_filename="6_pip_install_from_freeze.sh",
175
+ **common_args,
176
+ )
177
+ logger.info("end - install from pip freeze")
178
+ activity.log = get_current_log(log_file_path)
179
+ activity.status = TaskGroupActivityStatusV2.OK
180
+ activity.timestamp_ended = get_timestamp()
181
+ activity = add_commit_refresh(obj=activity, db=db)
182
+ task_group.active = True
183
+ task_group = add_commit_refresh(obj=task_group, db=db)
184
+ logger.info("END")
185
+
186
+ reset_logger_handlers(logger)
187
+
188
+ except Exception as reactivate_e:
189
+ # Delete corrupted venv_path
190
+ try:
191
+ logger.info(
192
+ f"Now delete folder {task_group.venv_path}"
193
+ )
194
+ fractal_ssh.remove_folder(
195
+ folder=task_group.venv_path,
196
+ safe_root=tasks_base_dir,
197
+ )
198
+ logger.info(f"Deleted folder {task_group.venv_path}")
199
+ except Exception as rm_e:
200
+ logger.error(
201
+ "Removing folder failed.\n"
202
+ f"Original error:\n{str(rm_e)}"
203
+ )
204
+
205
+ fail_and_cleanup(
206
+ task_group=task_group,
207
+ task_group_activity=activity,
208
+ logger_name=LOGGER_NAME,
209
+ log_file_path=log_file_path,
210
+ exception=reactivate_e,
211
+ db=db,
212
+ )
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
  from typing import TypeVar
4
3
 
5
4
  from sqlalchemy.orm import Session as DBSyncSession
@@ -53,7 +52,7 @@ def _prepare_tasks_metadata(
53
52
  package_manifest: ManifestV2,
54
53
  python_bin: Path,
55
54
  package_root: Path,
56
- package_version: Optional[str] = None,
55
+ package_version: str | None = None,
57
56
  ) -> list[TaskCreateV2]:
58
57
  """
59
58
  Based on the package manifest and additional info, prepare the task list.
@@ -101,5 +100,5 @@ def _prepare_tasks_metadata(
101
100
 
102
101
 
103
102
  def get_current_log(logger_file_path: str) -> str:
104
- with open(logger_file_path, "r") as f:
103
+ with open(logger_file_path) as f:
105
104
  return f.read()
@@ -1,6 +1,5 @@
1
1
  from typing import Annotated
2
2
  from typing import Any
3
- from typing import Optional
4
3
  from typing import Union
5
4
 
6
5
  from pydantic import AfterValidator
@@ -70,7 +69,7 @@ ImageAttributes = Annotated[
70
69
  AfterValidator(valdict_keys),
71
70
  ]
72
71
  ImageAttributesWithNone = Annotated[
73
- dict[str, Optional[ImageAttributeValue]],
72
+ dict[str, ImageAttributeValue | None],
74
73
  AfterValidator(valdict_keys),
75
74
  ]
76
75
  AttributeFilters = Annotated[
@@ -1,11 +1,10 @@
1
1
  from typing import Any
2
- from typing import Union
3
2
 
4
3
  from ._common_validators import valdict_keys
5
4
 
6
5
 
7
6
  def validate_attribute_filters(
8
- attribute_filters: dict[str, list[Union[int, float, str, bool]]]
7
+ attribute_filters: dict[str, list[int | float | str | bool]]
9
8
  ) -> dict[str, list[Any]]:
10
9
  attribute_filters = valdict_keys(attribute_filters)
11
10
  for key, values in attribute_filters.items():
fractal_server/utils.py CHANGED
@@ -19,7 +19,6 @@ import subprocess # nosec
19
19
  from datetime import datetime
20
20
  from datetime import timezone
21
21
  from pathlib import Path
22
- from typing import Optional
23
22
 
24
23
  from .logger import get_logger
25
24
  from .string_tools import validate_cmd
@@ -35,8 +34,8 @@ def get_timestamp() -> datetime:
35
34
  async def execute_command_async(
36
35
  *,
37
36
  command: str,
38
- cwd: Optional[Path] = None,
39
- logger_name: Optional[str] = None,
37
+ cwd: Path | None = None,
38
+ logger_name: str | None = None,
40
39
  ) -> str:
41
40
  """
42
41
  Execute arbitrary command
@@ -82,8 +81,8 @@ async def execute_command_async(
82
81
  def execute_command_sync(
83
82
  *,
84
83
  command: str,
85
- logger_name: Optional[str] = None,
86
- allow_char: Optional[str] = None,
84
+ logger_name: str | None = None,
85
+ allow_char: str | None = None,
87
86
  ) -> str:
88
87
  """
89
88
  Execute arbitrary command
@@ -1,8 +1,8 @@
1
1
  import os
2
2
  import shutil
3
+ from collections.abc import Iterator
3
4
  from io import BytesIO
4
5
  from pathlib import Path
5
- from typing import Iterator
6
6
  from typing import TypeVar
7
7
  from zipfile import ZIP_DEFLATED
8
8
  from zipfile import ZipFile
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fractal-server
3
- Version: 2.14.5
3
+ Version: 2.14.7
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License: BSD-3-Clause
6
6
  Author: Tommaso Comparin
7
7
  Author-email: tommaso.comparin@exact-lab.it
8
- Requires-Python: >=3.10,<3.13
8
+ Requires-Python: >=3.11,<3.13
9
9
  Classifier: License :: OSI Approved :: BSD License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  Requires-Dist: alembic (>=1.13.1,<2.0.0)