shinestacker 0.5.0__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +37 -16
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +3 -4
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +275 -371
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +2 -5
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +318 -40
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +34 -30
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
- shinestacker-1.0.1.dist-info/RECORD +91 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/retouch/image_editor.py +0 -197
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.5.0.dist-info/RECORD +0 -87
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,60 +1,153 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
|
|
2
|
-
import os.path
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, R0912, R0904, R0915, W0718
|
|
3
2
|
import os
|
|
4
|
-
|
|
3
|
+
import os.path
|
|
4
|
+
import traceback
|
|
5
5
|
import json
|
|
6
6
|
import jsonpickle
|
|
7
|
+
from PySide6.QtCore import Signal, QObject
|
|
7
8
|
from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
|
|
8
|
-
from .. core.core_utils import get_app_base_path
|
|
9
9
|
from .. config.constants import constants
|
|
10
|
+
from .. core.core_utils import get_app_base_path
|
|
10
11
|
from .project_model import ActionConfig
|
|
11
|
-
from .project_editor import ProjectEditor
|
|
12
12
|
from .new_project import NewProjectDialog
|
|
13
13
|
from .project_model import Project
|
|
14
|
+
from .project_editor import ProjectEditor
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
class ProjectController(QObject):
|
|
18
|
+
update_title_requested = Signal()
|
|
19
|
+
refresh_ui_requested = Signal(int, int)
|
|
20
|
+
activate_window_requested = Signal()
|
|
21
|
+
enable_save_actions_requested = Signal(bool)
|
|
22
|
+
|
|
23
|
+
def __init__(self, parent):
|
|
24
|
+
super().__init__(parent)
|
|
25
|
+
self.parent = parent
|
|
26
|
+
self.project_editor = ProjectEditor(parent)
|
|
27
|
+
|
|
28
|
+
def refresh_ui(self, job_row=-1, action_row=-1):
|
|
29
|
+
self.refresh_ui_requested.emit(job_row, action_row)
|
|
30
|
+
|
|
31
|
+
def mark_as_modified(self, modified=True):
|
|
32
|
+
self.project_editor.mark_as_modified(modified)
|
|
33
|
+
|
|
34
|
+
def modified(self):
|
|
35
|
+
return self.project_editor.modified()
|
|
36
|
+
|
|
37
|
+
def set_project(self, project):
|
|
38
|
+
self.project_editor.set_project(project)
|
|
39
|
+
|
|
40
|
+
def project(self):
|
|
41
|
+
return self.project_editor.project()
|
|
42
|
+
|
|
43
|
+
def project_jobs(self):
|
|
44
|
+
return self.project_editor.project_jobs()
|
|
45
|
+
|
|
46
|
+
def project_job(self, i):
|
|
47
|
+
return self.project_editor.project_job(i)
|
|
48
|
+
|
|
49
|
+
def add_job_to_project(self, job):
|
|
50
|
+
self.project_editor.add_job_to_project(job)
|
|
51
|
+
|
|
52
|
+
def num_project_jobs(self):
|
|
53
|
+
return self.project_editor.num_project_jobs()
|
|
54
|
+
|
|
55
|
+
def save_actions_set_enabled(self, enabled):
|
|
56
|
+
self.enable_save_actions_requested.emit(enabled)
|
|
57
|
+
|
|
58
|
+
def current_file_path(self):
|
|
59
|
+
return self.project_editor.current_file_path()
|
|
60
|
+
|
|
61
|
+
def current_file_directory(self):
|
|
62
|
+
return self.project_editor.current_file_directory()
|
|
63
|
+
|
|
64
|
+
def current_file_name(self):
|
|
65
|
+
return self.project_editor.current_file_name()
|
|
66
|
+
|
|
67
|
+
def set_current_file_path(self, path):
|
|
68
|
+
self.project_editor.set_current_file_path(path)
|
|
69
|
+
|
|
70
|
+
def job_list(self):
|
|
71
|
+
return self.project_editor.job_list()
|
|
72
|
+
|
|
73
|
+
def action_list(self):
|
|
74
|
+
return self.project_editor.action_list()
|
|
75
|
+
|
|
76
|
+
def current_job_index(self):
|
|
77
|
+
return self.project_editor.current_job_index()
|
|
78
|
+
|
|
79
|
+
def current_action_index(self):
|
|
80
|
+
return self.project_editor.current_action_index()
|
|
81
|
+
|
|
82
|
+
def set_current_job(self, index):
|
|
83
|
+
return self.project_editor.set_current_job(index)
|
|
84
|
+
|
|
85
|
+
def set_current_action(self, index):
|
|
86
|
+
return self.project_editor.set_current_action(index)
|
|
87
|
+
|
|
88
|
+
def job_list_count(self):
|
|
89
|
+
return self.project_editor.job_list_count()
|
|
90
|
+
|
|
91
|
+
def action_list_count(self):
|
|
92
|
+
return self.project_editor.action_list_count()
|
|
93
|
+
|
|
94
|
+
def job_list_item(self, index):
|
|
95
|
+
return self.project_editor.job_list_item(index)
|
|
96
|
+
|
|
97
|
+
def action_list_item(self, index):
|
|
98
|
+
return self.project_editor.action_list_item(index)
|
|
99
|
+
|
|
100
|
+
def job_list_has_focus(self):
|
|
101
|
+
return self.project_editor.job_list_has_focus()
|
|
102
|
+
|
|
103
|
+
def action_list_has_focus(self):
|
|
104
|
+
return self.project_editor.action_list_has_focus()
|
|
105
|
+
|
|
106
|
+
def clear_job_list(self):
|
|
107
|
+
self.project_editor.clear_job_list()
|
|
108
|
+
|
|
109
|
+
def clear_action_list(self):
|
|
110
|
+
self.project_editor.clear_action_list()
|
|
111
|
+
|
|
112
|
+
def num_selected_jobs(self):
|
|
113
|
+
return self.project_editor.num_selected_jobs()
|
|
114
|
+
|
|
115
|
+
def num_selected_actions(self):
|
|
116
|
+
return self.project_editor.num_selected_actions()
|
|
117
|
+
|
|
118
|
+
def get_current_action_at(self, job, action_index):
|
|
119
|
+
return self.project_editor.get_current_action_at(job, action_index)
|
|
120
|
+
|
|
121
|
+
def action_config_dialog(self, action):
|
|
122
|
+
return self.project_editor.action_config_dialog(action)
|
|
123
|
+
|
|
124
|
+
def on_job_selected(self, index):
|
|
125
|
+
return self.project_editor.on_job_selected(index)
|
|
20
126
|
|
|
21
127
|
def update_title(self):
|
|
22
|
-
|
|
23
|
-
file_name = self.current_file_name()
|
|
24
|
-
if file_name:
|
|
25
|
-
title += f" - {file_name}"
|
|
26
|
-
if self._modified_project:
|
|
27
|
-
title += " *"
|
|
28
|
-
self.window().setWindowTitle(title)
|
|
29
|
-
|
|
30
|
-
def mark_as_modified(self):
|
|
31
|
-
self._modified_project = True
|
|
32
|
-
self.project_buffer.append(self.project.clone())
|
|
33
|
-
self.save_actions_set_enabled(True)
|
|
34
|
-
self.update_title()
|
|
128
|
+
self.update_title_requested.emit()
|
|
35
129
|
|
|
36
130
|
def close_project(self):
|
|
37
|
-
if self.
|
|
131
|
+
if self.check_unsaved_changes():
|
|
38
132
|
self.set_project(Project())
|
|
39
133
|
self.set_current_file_path('')
|
|
40
134
|
self.update_title()
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.save_actions_set_enabled(False)
|
|
135
|
+
self.clear_job_list()
|
|
136
|
+
self.clear_action_list()
|
|
137
|
+
self.mark_as_modified(False)
|
|
45
138
|
|
|
46
139
|
def new_project(self):
|
|
47
|
-
if not self.
|
|
140
|
+
if not self.check_unsaved_changes():
|
|
48
141
|
return
|
|
49
142
|
os.chdir(get_app_base_path())
|
|
50
143
|
self.set_current_file_path('')
|
|
51
|
-
self.
|
|
144
|
+
self.mark_as_modified(False)
|
|
52
145
|
self.update_title()
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
146
|
+
self.clear_job_list()
|
|
147
|
+
self.clear_action_list()
|
|
55
148
|
self.set_project(Project())
|
|
56
149
|
self.save_actions_set_enabled(False)
|
|
57
|
-
dialog = NewProjectDialog(self)
|
|
150
|
+
dialog = NewProjectDialog(self.parent)
|
|
58
151
|
if dialog.exec() == QDialog.Accepted:
|
|
59
152
|
self.save_actions_set_enabled(True)
|
|
60
153
|
input_folder = dialog.get_input_folder().split('/')
|
|
@@ -67,7 +160,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
67
160
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
68
161
|
{'name': 'detect-noise'})
|
|
69
162
|
job_noise.add_sub_action(noise_detection)
|
|
70
|
-
self.
|
|
163
|
+
self.add_job_to_project(job_noise)
|
|
71
164
|
job = ActionConfig(constants.ACTION_JOB,
|
|
72
165
|
{'name': 'focus-stack', 'working_path': working_path,
|
|
73
166
|
'input_path': input_path})
|
|
@@ -116,16 +209,16 @@ class ActionsWindow(ProjectEditor):
|
|
|
116
209
|
{'name': 'multi-layer',
|
|
117
210
|
'input_path': ','.join(input_path)})
|
|
118
211
|
job.add_sub_action(multi_layer)
|
|
119
|
-
self.
|
|
120
|
-
self.
|
|
212
|
+
self.add_job_to_project(job)
|
|
213
|
+
self.mark_as_modified(True)
|
|
121
214
|
self.refresh_ui(0, -1)
|
|
122
215
|
|
|
123
216
|
def open_project(self, file_path=False):
|
|
124
|
-
if not self.
|
|
217
|
+
if not self.check_unsaved_changes():
|
|
125
218
|
return
|
|
126
219
|
if file_path is False:
|
|
127
220
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
128
|
-
self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
221
|
+
self.parent, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
129
222
|
if file_path:
|
|
130
223
|
try:
|
|
131
224
|
self.set_current_file_path(file_path)
|
|
@@ -135,24 +228,24 @@ class ActionsWindow(ProjectEditor):
|
|
|
135
228
|
if project is None:
|
|
136
229
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
137
230
|
self.set_project(project)
|
|
138
|
-
self.
|
|
139
|
-
self.update_title()
|
|
231
|
+
self.mark_as_modified(False)
|
|
140
232
|
self.refresh_ui(0, -1)
|
|
141
|
-
if self.
|
|
142
|
-
self.
|
|
233
|
+
if self.job_list_count() > 0:
|
|
234
|
+
self.set_current_job(0)
|
|
143
235
|
except Exception as e:
|
|
144
|
-
|
|
145
|
-
QMessageBox.critical(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
self.
|
|
236
|
+
traceback.print_tb(e.__traceback__)
|
|
237
|
+
QMessageBox.critical(
|
|
238
|
+
self.parent, "Error", f"Cannot open file {file_path}:\n{str(e)}")
|
|
239
|
+
if self.num_project_jobs() > 0:
|
|
240
|
+
self.set_current_job(0)
|
|
241
|
+
self.activate_window_requested.emit()
|
|
149
242
|
self.save_actions_set_enabled(True)
|
|
150
|
-
for job in self.
|
|
243
|
+
for job in self.project_jobs():
|
|
151
244
|
if 'working_path' in job.params.keys():
|
|
152
245
|
working_path = job.params['working_path']
|
|
153
246
|
if not os.path.isdir(working_path):
|
|
154
247
|
QMessageBox.warning(
|
|
155
|
-
self, "Working path not found",
|
|
248
|
+
self.parent, "Working path not found",
|
|
156
249
|
f'''The working path specified in the project file for the job:
|
|
157
250
|
"{job.params['name']}"
|
|
158
251
|
was not found.\n
|
|
@@ -163,7 +256,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
163
256
|
working_path = job.params['working_path']
|
|
164
257
|
if working_path != '' and not os.path.isdir(working_path):
|
|
165
258
|
QMessageBox.warning(
|
|
166
|
-
self, "Working path not found",
|
|
259
|
+
self.parent, "Working path not found",
|
|
167
260
|
f'''The working path specified in the project file for the job:
|
|
168
261
|
"{job.params['name']}"
|
|
169
262
|
was not found.\n
|
|
@@ -179,32 +272,29 @@ class ActionsWindow(ProjectEditor):
|
|
|
179
272
|
|
|
180
273
|
def save_project_as(self):
|
|
181
274
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
182
|
-
self, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
|
|
275
|
+
self.parent, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
|
|
183
276
|
if file_path:
|
|
184
277
|
if not file_path.endswith('.fsp'):
|
|
185
278
|
file_path += '.fsp'
|
|
186
279
|
self.do_save(file_path)
|
|
187
280
|
self.set_current_file_path(file_path)
|
|
188
|
-
self._modified_project = False
|
|
189
|
-
self.update_title()
|
|
190
281
|
os.chdir(os.path.dirname(file_path))
|
|
191
282
|
|
|
192
283
|
def do_save(self, file_path):
|
|
193
284
|
try:
|
|
194
285
|
json_obj = jsonpickle.encode({
|
|
195
|
-
'project': self.project.to_dict(),
|
|
196
|
-
'version': 1
|
|
286
|
+
'project': self.project().to_dict(), 'version': 1
|
|
197
287
|
})
|
|
198
288
|
with open(file_path, 'w', encoding="utf-8") as f:
|
|
199
289
|
f.write(json_obj)
|
|
200
|
-
self.
|
|
290
|
+
self.mark_as_modified(False)
|
|
201
291
|
except Exception as e:
|
|
202
|
-
QMessageBox.critical(self, "Error", f"Cannot save file:\n{str(e)}")
|
|
292
|
+
QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
|
|
203
293
|
|
|
204
|
-
def
|
|
205
|
-
if self.
|
|
294
|
+
def check_unsaved_changes(self) -> bool:
|
|
295
|
+
if self.modified():
|
|
206
296
|
reply = QMessageBox.question(
|
|
207
|
-
self, "Unsaved Changes",
|
|
297
|
+
self.parent, "Unsaved Changes",
|
|
208
298
|
"The project has unsaved changes. Do you want to continue?",
|
|
209
299
|
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
|
210
300
|
)
|
|
@@ -215,21 +305,21 @@ class ActionsWindow(ProjectEditor):
|
|
|
215
305
|
return True
|
|
216
306
|
|
|
217
307
|
def on_job_edit(self, item):
|
|
218
|
-
index = self.job_list.row(item)
|
|
219
|
-
if 0 <= index <
|
|
220
|
-
job = self.
|
|
308
|
+
index = self.job_list().row(item)
|
|
309
|
+
if 0 <= index < self.num_project_jobs():
|
|
310
|
+
job = self.project_job(index)
|
|
221
311
|
dialog = self.action_config_dialog(job)
|
|
222
312
|
if dialog.exec() == QDialog.Accepted:
|
|
223
|
-
current_row = self.
|
|
313
|
+
current_row = self.current_job_index()
|
|
224
314
|
if current_row >= 0:
|
|
225
|
-
self.
|
|
315
|
+
self.job_list_item(current_row).setText(job.params['name'])
|
|
226
316
|
self.refresh_ui()
|
|
227
317
|
|
|
228
318
|
def on_action_edit(self, item):
|
|
229
|
-
job_index = self.
|
|
230
|
-
if 0 <= job_index <
|
|
231
|
-
job = self.
|
|
232
|
-
action_index = self.action_list.row(item)
|
|
319
|
+
job_index = self.current_job_index()
|
|
320
|
+
if 0 <= job_index < self.num_project_jobs():
|
|
321
|
+
job = self.project_job(job_index)
|
|
322
|
+
action_index = self.action_list().row(item)
|
|
233
323
|
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
234
324
|
if current_action:
|
|
235
325
|
if not is_sub_action:
|
|
@@ -239,17 +329,17 @@ class ActionsWindow(ProjectEditor):
|
|
|
239
329
|
if dialog.exec() == QDialog.Accepted:
|
|
240
330
|
self.on_job_selected(job_index)
|
|
241
331
|
self.refresh_ui()
|
|
242
|
-
self.
|
|
243
|
-
self.
|
|
332
|
+
self.set_current_job(job_index)
|
|
333
|
+
self.set_current_action(action_index)
|
|
244
334
|
|
|
245
335
|
def edit_current_action(self):
|
|
246
336
|
current_action = None
|
|
247
|
-
job_row = self.
|
|
248
|
-
if 0 <= job_row <
|
|
249
|
-
job = self.
|
|
250
|
-
if self.
|
|
337
|
+
job_row = self.current_job_index()
|
|
338
|
+
if 0 <= job_row < self.num_project_jobs():
|
|
339
|
+
job = self.project_job(job_row)
|
|
340
|
+
if self.job_list_has_focus():
|
|
251
341
|
current_action = job
|
|
252
|
-
elif self.
|
|
342
|
+
elif self.action_list_has_focus():
|
|
253
343
|
job_row, _action_row, pos = self.get_current_action()
|
|
254
344
|
if pos.actions is not None:
|
|
255
345
|
current_action = pos.action if not pos.is_sub_action else pos.sub_action
|
|
@@ -259,8 +349,5 @@ class ActionsWindow(ProjectEditor):
|
|
|
259
349
|
def edit_action(self, action):
|
|
260
350
|
dialog = self.action_config_dialog(action)
|
|
261
351
|
if dialog.exec() == QDialog.Accepted:
|
|
262
|
-
self.on_job_selected(self.
|
|
352
|
+
self.on_job_selected(self.current_job_index())
|
|
263
353
|
self.mark_as_modified()
|
|
264
|
-
|
|
265
|
-
def save_actions_set_enabled(self, enabled):
|
|
266
|
-
pass
|
|
@@ -36,10 +36,10 @@ class ProjectConverter:
|
|
|
36
36
|
logger.error(f"=== job: {job.name} failed: {msg} ===")
|
|
37
37
|
return constants.RUN_FAILED, msg
|
|
38
38
|
|
|
39
|
-
def run_project(self,
|
|
39
|
+
def run_project(self, proj: Project, logger_name=None, callbacks=None):
|
|
40
40
|
logger = self.get_logger(logger_name)
|
|
41
41
|
try:
|
|
42
|
-
jobs = self.project(
|
|
42
|
+
jobs = self.project(proj, logger_name, callbacks)
|
|
43
43
|
except Exception as e:
|
|
44
44
|
traceback.print_tb(e.__traceback__)
|
|
45
45
|
return constants.RUN_FAILED, str(e)
|
|
@@ -60,12 +60,12 @@ class ProjectConverter:
|
|
|
60
60
|
status = self.run(job, logger)
|
|
61
61
|
return status
|
|
62
62
|
|
|
63
|
-
def project(self,
|
|
63
|
+
def project(self, proj: Project, logger_name=None, callbacks=None):
|
|
64
64
|
jobs = []
|
|
65
|
-
for j in
|
|
65
|
+
for j in proj.jobs:
|
|
66
66
|
job = self.job(j, logger_name, callbacks)
|
|
67
67
|
if job is None:
|
|
68
|
-
raise RuntimeError("Job
|
|
68
|
+
raise RuntimeError("Job creation failed.")
|
|
69
69
|
jobs.append(job)
|
|
70
70
|
return jobs
|
|
71
71
|
|
|
@@ -143,6 +143,6 @@ class ProjectConverter:
|
|
|
143
143
|
except Exception as e:
|
|
144
144
|
msg = str(e)
|
|
145
145
|
logger = self.get_logger(logger_name)
|
|
146
|
-
logger.error(msg=f"=== can't
|
|
146
|
+
logger.error(msg=f"=== can't create job: {name}: {msg} ===")
|
|
147
147
|
traceback.print_tb(e.__traceback__)
|
|
148
148
|
raise e
|