shinestacker 0.2.0.post1.dev1__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/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
|
|
3
|
+
QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
|
|
4
|
+
QSpinBox, QMessageBox)
|
|
5
|
+
from PySide6.QtGui import QIcon
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from .. config.gui_constants import gui_constants
|
|
8
|
+
from .. config.constants import constants
|
|
9
|
+
from .. core.core_utils import get_app_base_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NewProjectDialog(QDialog):
|
|
13
|
+
def __init__(self, parent=None):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self.setWindowTitle("New Project")
|
|
16
|
+
self.resize(500, self.height())
|
|
17
|
+
self.layout = QFormLayout(self)
|
|
18
|
+
self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
19
|
+
self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
20
|
+
self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
21
|
+
self.layout.setLabelAlignment(Qt.AlignLeft)
|
|
22
|
+
self.create_form()
|
|
23
|
+
button_box = QHBoxLayout()
|
|
24
|
+
ok_button = QPushButton("OK")
|
|
25
|
+
ok_button.setFocus()
|
|
26
|
+
cancel_button = QPushButton("Cancel")
|
|
27
|
+
button_box.addWidget(ok_button)
|
|
28
|
+
button_box.addWidget(cancel_button)
|
|
29
|
+
self.layout.addRow(button_box)
|
|
30
|
+
ok_button.clicked.connect(self.accept)
|
|
31
|
+
cancel_button.clicked.connect(self.reject)
|
|
32
|
+
|
|
33
|
+
def expert(self):
|
|
34
|
+
return self.parent().expert_options
|
|
35
|
+
|
|
36
|
+
def add_bold_label(self, label):
|
|
37
|
+
label = QLabel(label)
|
|
38
|
+
label.setStyleSheet("font-weight: bold")
|
|
39
|
+
self.layout.addRow(label)
|
|
40
|
+
|
|
41
|
+
def create_form(self):
|
|
42
|
+
icon_path = f'{get_app_base_path()}'
|
|
43
|
+
if os.path.exists(f'{icon_path}/ico'):
|
|
44
|
+
icon_path = f'{icon_path}/ico'
|
|
45
|
+
else:
|
|
46
|
+
icon_path = f'{icon_path}/../ico'
|
|
47
|
+
icon_path = f'{icon_path}/shinestacker.png'
|
|
48
|
+
app_icon = QIcon(icon_path)
|
|
49
|
+
icon_pixmap = app_icon.pixmap(128, 128)
|
|
50
|
+
icon_label = QLabel()
|
|
51
|
+
icon_label.setPixmap(icon_pixmap)
|
|
52
|
+
icon_label.setAlignment(Qt.AlignCenter)
|
|
53
|
+
self.layout.addRow(icon_label)
|
|
54
|
+
spacer = QLabel("")
|
|
55
|
+
spacer.setFixedHeight(10)
|
|
56
|
+
self.layout.addRow(spacer)
|
|
57
|
+
self.input_folder = QLineEdit()
|
|
58
|
+
self.input_folder .setPlaceholderText('input files folder')
|
|
59
|
+
button = QPushButton("Browse...")
|
|
60
|
+
|
|
61
|
+
def browse():
|
|
62
|
+
path = QFileDialog.getExistingDirectory(None, "Select input files folder")
|
|
63
|
+
if path:
|
|
64
|
+
self.input_folder.setText(path)
|
|
65
|
+
button.clicked.connect(browse)
|
|
66
|
+
button.setAutoDefault(False)
|
|
67
|
+
layout = QHBoxLayout()
|
|
68
|
+
layout.addWidget(self.input_folder)
|
|
69
|
+
layout.addWidget(button)
|
|
70
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
71
|
+
container = QWidget()
|
|
72
|
+
container.setLayout(layout)
|
|
73
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
74
|
+
|
|
75
|
+
self.noise_detection = QCheckBox()
|
|
76
|
+
self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
|
|
77
|
+
self.vignetting_correction = QCheckBox()
|
|
78
|
+
self.vignetting_correction.setChecked(gui_constants.NEW_PROJECT_VIGNETTING_CORRECTION)
|
|
79
|
+
self.align_frames = QCheckBox()
|
|
80
|
+
self.align_frames.setChecked(gui_constants.NEW_PROJECT_ALIGN_FRAMES)
|
|
81
|
+
self.balance_frames = QCheckBox()
|
|
82
|
+
self.balance_frames.setChecked(gui_constants.NEW_PROJECT_BALANCE_FRAMES)
|
|
83
|
+
self.bunch_stack = QCheckBox()
|
|
84
|
+
self.bunch_stack.setChecked(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
85
|
+
self.bunch_stack.toggled.connect(self.update_bunch_options)
|
|
86
|
+
self.bunch_frames = QSpinBox()
|
|
87
|
+
bunch_frames_range = gui_constants.NEW_PROJECT_BUNCH_FRAMES
|
|
88
|
+
self.bunch_frames.setRange(bunch_frames_range['min'], bunch_frames_range['max'])
|
|
89
|
+
self.bunch_frames.setValue(constants.DEFAULT_FRAMES)
|
|
90
|
+
self.bunch_overlap = QSpinBox()
|
|
91
|
+
bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
|
|
92
|
+
self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
|
|
93
|
+
self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
|
|
94
|
+
self.focus_stack_pyramid = QCheckBox()
|
|
95
|
+
self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
|
|
96
|
+
self.focus_stack_depth_map = QCheckBox()
|
|
97
|
+
self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
|
|
98
|
+
self.multi_layer = QCheckBox()
|
|
99
|
+
self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
|
|
100
|
+
|
|
101
|
+
self.add_bold_label("Select input:")
|
|
102
|
+
self.layout.addRow("Input folder:", container)
|
|
103
|
+
self.add_bold_label("Select actions:")
|
|
104
|
+
if self.expert():
|
|
105
|
+
self.layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
106
|
+
self.layout.addRow("Vignetting correction:", self.vignetting_correction)
|
|
107
|
+
self.layout.addRow("Align layers:", self.align_frames)
|
|
108
|
+
self.layout.addRow("Balance layers:", self.balance_frames)
|
|
109
|
+
self.layout.addRow("Bunch stack:", self.bunch_stack)
|
|
110
|
+
self.layout.addRow("Bunch frames:", self.bunch_frames)
|
|
111
|
+
self.layout.addRow("Bunch frames:", self.bunch_overlap)
|
|
112
|
+
if self.expert():
|
|
113
|
+
self.layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
|
|
114
|
+
self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
|
|
115
|
+
else:
|
|
116
|
+
self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
|
|
117
|
+
self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
|
|
118
|
+
|
|
119
|
+
def update_bunch_options(self, checked):
|
|
120
|
+
self.bunch_frames.setEnabled(checked)
|
|
121
|
+
self.bunch_overlap.setEnabled(checked)
|
|
122
|
+
|
|
123
|
+
def accept(self):
|
|
124
|
+
input_folder = self.input_folder.text()
|
|
125
|
+
if not input_folder:
|
|
126
|
+
QMessageBox.warning(self, "Input Required", "Please select an input folder")
|
|
127
|
+
return
|
|
128
|
+
if not os.path.exists(input_folder):
|
|
129
|
+
QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
|
|
130
|
+
return
|
|
131
|
+
if not os.path.isdir(input_folder):
|
|
132
|
+
QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
|
|
133
|
+
return
|
|
134
|
+
if len(input_folder.split('/')) < 2:
|
|
135
|
+
QMessageBox.warning(self, "Invalid Path", "The path must have a parent folder")
|
|
136
|
+
return
|
|
137
|
+
super().accept()
|
|
138
|
+
|
|
139
|
+
def get_input_folder(self):
|
|
140
|
+
return self.input_folder.text()
|
|
141
|
+
|
|
142
|
+
def get_noise_detection(self):
|
|
143
|
+
return self.noise_detection.isChecked()
|
|
144
|
+
|
|
145
|
+
def get_vignetting_correction(self):
|
|
146
|
+
return self.vignetting_correction.isChecked()
|
|
147
|
+
|
|
148
|
+
def get_align_frames(self):
|
|
149
|
+
return self.align_frames.isChecked()
|
|
150
|
+
|
|
151
|
+
def get_balance_frames(self):
|
|
152
|
+
return self.balance_frames.isChecked()
|
|
153
|
+
|
|
154
|
+
def get_bunch_stack(self):
|
|
155
|
+
return self.bunch_stack.isChecked()
|
|
156
|
+
|
|
157
|
+
def get_bunch_frames(self):
|
|
158
|
+
return self.bunch_frames.value()
|
|
159
|
+
|
|
160
|
+
def get_bunch_overlap(self):
|
|
161
|
+
return self.bunch_overlap.value()
|
|
162
|
+
|
|
163
|
+
def get_focus_stack_pyramid(self):
|
|
164
|
+
return self.focus_stack_pyramid.isChecked()
|
|
165
|
+
|
|
166
|
+
def get_focus_stack_depth_map(self):
|
|
167
|
+
return self.focus_stack_depth_map.isChecked()
|
|
168
|
+
|
|
169
|
+
def get_multi_layer(self):
|
|
170
|
+
return self.multi_layer.isChecked()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import traceback
|
|
3
|
+
from .. config.constants import constants
|
|
4
|
+
from .. core.exceptions import InvalidOptionError, RunStopException
|
|
5
|
+
from .. algorithms.stack_framework import StackJob, CombinedActions
|
|
6
|
+
from .. algorithms.noise_detection import NoiseDetection, MaskNoise
|
|
7
|
+
from .. algorithms.vignetting import Vignetting
|
|
8
|
+
from .. algorithms.align import AlignFrames
|
|
9
|
+
from .. algorithms.balance import BalanceFrames
|
|
10
|
+
from .. algorithms.stack import FocusStack, FocusStackBunch
|
|
11
|
+
from .. algorithms.pyramid import PyramidStack
|
|
12
|
+
from .. algorithms.depth_map import DepthMapStack
|
|
13
|
+
from .. algorithms.multilayer import MultiLayer
|
|
14
|
+
from .project_model import Project, ActionConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProjectConverter:
|
|
18
|
+
def get_logger(self, logger_name=None):
|
|
19
|
+
return logging.getLogger(__name__ if logger_name is None else logger_name)
|
|
20
|
+
|
|
21
|
+
def run(self, job, logger):
|
|
22
|
+
if job.enabled:
|
|
23
|
+
logger.info(f"=== run job: {job.name} ===")
|
|
24
|
+
else:
|
|
25
|
+
logger.warning(f"=== job: {job.name} disabled ===")
|
|
26
|
+
try:
|
|
27
|
+
job.run()
|
|
28
|
+
return constants.RUN_COMPLETED, ''
|
|
29
|
+
except RunStopException:
|
|
30
|
+
logger.warning(f"=== job: {job.name} stopped ===")
|
|
31
|
+
return constants.RUN_STOPPED, ''
|
|
32
|
+
except Exception as e:
|
|
33
|
+
traceback.print_tb(e.__traceback__)
|
|
34
|
+
msg = str(e)
|
|
35
|
+
logger.error(f"=== job: {job.name} failed: {msg} ===")
|
|
36
|
+
return constants.RUN_FAILED, msg
|
|
37
|
+
|
|
38
|
+
def run_project(self, project: Project, logger_name=None, callbacks=None):
|
|
39
|
+
logger = self.get_logger(logger_name)
|
|
40
|
+
try:
|
|
41
|
+
jobs = self.project(project, logger_name, callbacks)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
traceback.print_tb(e.__traceback__)
|
|
44
|
+
return constants.RUN_FAILED, str(e)
|
|
45
|
+
status = constants.RUN_COMPLETED, ''
|
|
46
|
+
for job in jobs:
|
|
47
|
+
job_status, message = self.run(job, logger)
|
|
48
|
+
if job_status in [constants.RUN_STOPPED, constants.RUN_FAILED]:
|
|
49
|
+
return job_status, message
|
|
50
|
+
return status
|
|
51
|
+
|
|
52
|
+
def run_job(self, job: ActionConfig, logger_name=None, callbacks=None):
|
|
53
|
+
logger = self.get_logger(logger_name)
|
|
54
|
+
try:
|
|
55
|
+
job = self.job(job, logger_name, callbacks)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
traceback.print_tb(e.__traceback__)
|
|
58
|
+
return constants.RUN_FAILED, str(e)
|
|
59
|
+
status = self.run(job, logger)
|
|
60
|
+
return status
|
|
61
|
+
|
|
62
|
+
def project(self, project: Project, logger_name=None, callbacks=None):
|
|
63
|
+
jobs = []
|
|
64
|
+
for j in project.jobs:
|
|
65
|
+
job = self.job(j, logger_name, callbacks)
|
|
66
|
+
if job is None:
|
|
67
|
+
raise Exception("Job instantiation failed.")
|
|
68
|
+
else:
|
|
69
|
+
jobs.append(job)
|
|
70
|
+
return jobs
|
|
71
|
+
|
|
72
|
+
def filter_dict_keys(self, dict, prefix):
|
|
73
|
+
dict_with = {k.replace(prefix, ''): v for (k, v) in dict.items() if k.startswith(prefix)}
|
|
74
|
+
dict_without = {k: v for (k, v) in dict.items() if not k.startswith(prefix)}
|
|
75
|
+
return dict_with, dict_without
|
|
76
|
+
|
|
77
|
+
def action(self, action_config):
|
|
78
|
+
if action_config.type_name == constants.ACTION_NOISEDETECTION:
|
|
79
|
+
return NoiseDetection(**action_config.params)
|
|
80
|
+
elif action_config.type_name == constants.ACTION_COMBO:
|
|
81
|
+
sub_actions = []
|
|
82
|
+
for sa in action_config.sub_actions:
|
|
83
|
+
a = self.action(sa)
|
|
84
|
+
if a is not None:
|
|
85
|
+
sub_actions.append(a)
|
|
86
|
+
a = CombinedActions(**action_config.params, actions=sub_actions)
|
|
87
|
+
return a
|
|
88
|
+
elif action_config.type_name == constants.ACTION_MASKNOISE:
|
|
89
|
+
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
90
|
+
return MaskNoise(**params)
|
|
91
|
+
elif action_config.type_name == constants.ACTION_VIGNETTING:
|
|
92
|
+
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
93
|
+
return Vignetting(**params)
|
|
94
|
+
elif action_config.type_name == constants.ACTION_ALIGNFRAMES:
|
|
95
|
+
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
96
|
+
return AlignFrames(**params)
|
|
97
|
+
elif action_config.type_name == constants.ACTION_BALANCEFRAMES:
|
|
98
|
+
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
99
|
+
if 'intensity_interval' in params.keys():
|
|
100
|
+
i = params['intensity_interval']
|
|
101
|
+
params['intensity_interval'] = {'min': i[0], 'max': i[1]}
|
|
102
|
+
return BalanceFrames(**params)
|
|
103
|
+
elif action_config.type_name == constants.ACTION_FOCUSSTACK or action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
|
|
104
|
+
stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
|
|
105
|
+
if stacker == constants.STACK_ALGO_PYRAMID:
|
|
106
|
+
algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'pyramid_')
|
|
107
|
+
stack_algo = PyramidStack(**algo_dict)
|
|
108
|
+
elif stacker == constants.STACK_ALGO_DEPTH_MAP:
|
|
109
|
+
algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'depthmap_')
|
|
110
|
+
stack_algo = DepthMapStack(**algo_dict)
|
|
111
|
+
else:
|
|
112
|
+
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
113
|
+
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
114
|
+
f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
|
|
115
|
+
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
116
|
+
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
117
|
+
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
118
|
+
elif action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
|
|
119
|
+
return FocusStackBunch(**module_dict, stack_algo=stack_algo)
|
|
120
|
+
else:
|
|
121
|
+
raise InvalidOptionError("stracker", stacker, details="valid values are: Pyramid, Depth map.")
|
|
122
|
+
elif action_config.type_name == constants.ACTION_MULTILAYER:
|
|
123
|
+
input_path = list(filter(lambda p: p != '', action_config.params.get('input_path', '').split(";")))
|
|
124
|
+
params = {k: v for k, v in action_config.params.items() if k != 'imput_path'}
|
|
125
|
+
params['input_path'] = [i.strip() for i in input_path]
|
|
126
|
+
return MultiLayer(**params)
|
|
127
|
+
else:
|
|
128
|
+
raise Exception(f"Cannot convert action of type {action_config.type_name}.")
|
|
129
|
+
|
|
130
|
+
def job(self, action_config: ActionConfig, logger_name=None, callbacks=None):
|
|
131
|
+
try:
|
|
132
|
+
name = action_config.params.get('name', '')
|
|
133
|
+
enabled = action_config.params.get('enabled', True)
|
|
134
|
+
working_path = action_config.params.get('working_path', '')
|
|
135
|
+
input_path = action_config.params.get('input_path', '')
|
|
136
|
+
stack_job = StackJob(name, working_path, enabled=enabled, input_path=input_path,
|
|
137
|
+
logger_name=logger_name, callbacks=callbacks)
|
|
138
|
+
for sub in action_config.sub_actions:
|
|
139
|
+
action = self.action(sub)
|
|
140
|
+
if action is not None:
|
|
141
|
+
stack_job.add_action(action)
|
|
142
|
+
return stack_job
|
|
143
|
+
except Exception as e:
|
|
144
|
+
msg = str(e)
|
|
145
|
+
logger = self.get_logger(logger_name)
|
|
146
|
+
logger.error(f"=== can't instantiate job: {name}: {msg} ===")
|
|
147
|
+
traceback.print_tb(e.__traceback__)
|
|
148
|
+
raise e
|