learning-loop-node 0.9.2__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of learning-loop-node might be problematic. Click here for more details.
- learning_loop_node/__init__.py +2 -3
- learning_loop_node/annotation/annotator_logic.py +2 -2
- learning_loop_node/annotation/annotator_node.py +16 -15
- learning_loop_node/data_classes/__init__.py +17 -10
- learning_loop_node/data_classes/detections.py +7 -2
- learning_loop_node/data_classes/general.py +8 -5
- learning_loop_node/data_classes/training.py +49 -21
- learning_loop_node/data_exchanger.py +85 -139
- learning_loop_node/detector/__init__.py +0 -1
- learning_loop_node/detector/detector_logic.py +0 -2
- learning_loop_node/detector/detector_node.py +14 -15
- learning_loop_node/detector/inbox_filter/cam_observation_history.py +4 -7
- learning_loop_node/detector/outbox.py +0 -1
- learning_loop_node/detector/rest/about.py +25 -0
- learning_loop_node/detector/tests/conftest.py +4 -1
- learning_loop_node/detector/tests/test_client_communication.py +18 -0
- learning_loop_node/detector/tests/test_outbox.py +2 -0
- learning_loop_node/detector/tests/testing_detector.py +0 -7
- learning_loop_node/globals.py +2 -2
- learning_loop_node/helpers/gdrive_downloader.py +1 -1
- learning_loop_node/helpers/misc.py +124 -17
- learning_loop_node/loop_communication.py +57 -25
- learning_loop_node/node.py +62 -135
- learning_loop_node/tests/test_downloader.py +8 -7
- learning_loop_node/tests/test_executor.py +14 -11
- learning_loop_node/tests/test_helper.py +3 -5
- learning_loop_node/trainer/downloader.py +1 -1
- learning_loop_node/trainer/executor.py +87 -83
- learning_loop_node/trainer/io_helpers.py +66 -9
- learning_loop_node/trainer/rest/backdoor_controls.py +10 -5
- learning_loop_node/trainer/rest/controls.py +3 -1
- learning_loop_node/trainer/tests/conftest.py +19 -28
- learning_loop_node/trainer/tests/states/test_state_cleanup.py +5 -3
- learning_loop_node/trainer/tests/states/test_state_detecting.py +23 -20
- learning_loop_node/trainer/tests/states/test_state_download_train_model.py +18 -12
- learning_loop_node/trainer/tests/states/test_state_prepare.py +13 -12
- learning_loop_node/trainer/tests/states/test_state_sync_confusion_matrix.py +21 -18
- learning_loop_node/trainer/tests/states/test_state_train.py +27 -28
- learning_loop_node/trainer/tests/states/test_state_upload_detections.py +34 -32
- learning_loop_node/trainer/tests/states/test_state_upload_model.py +22 -20
- learning_loop_node/trainer/tests/test_errors.py +20 -12
- learning_loop_node/trainer/tests/test_trainer_states.py +4 -5
- learning_loop_node/trainer/tests/testing_trainer_logic.py +25 -30
- learning_loop_node/trainer/trainer_logic.py +80 -590
- learning_loop_node/trainer/trainer_logic_generic.py +495 -0
- learning_loop_node/trainer/trainer_node.py +27 -106
- {learning_loop_node-0.9.2.dist-info → learning_loop_node-0.10.0.dist-info}/METADATA +1 -1
- learning_loop_node-0.10.0.dist-info/RECORD +85 -0
- learning_loop_node/converter/converter_logic.py +0 -68
- learning_loop_node/converter/converter_node.py +0 -125
- learning_loop_node/converter/tests/test_converter.py +0 -55
- learning_loop_node/trainer/training_syncronizer.py +0 -52
- learning_loop_node-0.9.2.dist-info/RECORD +0 -87
- /learning_loop_node/{converter/__init__.py → py.typed} +0 -0
- {learning_loop_node-0.9.2.dist-info → learning_loop_node-0.10.0.dist-info}/WHEEL +0 -0
learning_loop_node/__init__.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
2
|
|
|
5
|
-
from .converter.converter_node import ConverterNode
|
|
6
3
|
# from . import log_conf
|
|
7
4
|
from .detector.detector_logic import DetectorLogic
|
|
8
5
|
from .detector.detector_node import DetectorNode
|
|
9
6
|
from .globals import GLOBALS
|
|
10
7
|
from .trainer.trainer_node import TrainerNode
|
|
11
8
|
|
|
9
|
+
__all__ = ['TrainerNode', 'DetectorNode', 'DetectorLogic', 'GLOBALS']
|
|
10
|
+
|
|
12
11
|
logging.info('>>>>>>>>>>>>>>>>>> LOOP INITIALIZED <<<<<<<<<<<<<<<<<<<<<<<')
|
|
@@ -7,10 +7,10 @@ from ..node import Node
|
|
|
7
7
|
|
|
8
8
|
class AnnotatorLogic():
|
|
9
9
|
|
|
10
|
-
def __init__(self):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
11
|
self._node: Optional[Node] = None
|
|
12
12
|
|
|
13
|
-
def init(self, node: Node):
|
|
13
|
+
def init(self, node: Node) -> None:
|
|
14
14
|
self._node = node
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|
|
@@ -8,7 +8,7 @@ from socketio import AsyncClient
|
|
|
8
8
|
from ..data_classes import AnnotationNodeStatus, Context, NodeState, UserInput
|
|
9
9
|
from ..data_classes.socket_response import SocketResponse
|
|
10
10
|
from ..data_exchanger import DataExchanger
|
|
11
|
-
from ..helpers.misc import create_image_folder
|
|
11
|
+
from ..helpers.misc import create_image_folder, create_project_folder
|
|
12
12
|
from ..node import Node
|
|
13
13
|
from .annotator_logic import AnnotatorLogic
|
|
14
14
|
|
|
@@ -18,10 +18,11 @@ from .annotator_logic import AnnotatorLogic
|
|
|
18
18
|
class AnnotatorNode(Node):
|
|
19
19
|
|
|
20
20
|
def __init__(self, name: str, annotator_logic: AnnotatorLogic, uuid: Optional[str] = None):
|
|
21
|
-
super().__init__(name, uuid)
|
|
21
|
+
super().__init__(name, uuid, 'annotation_node')
|
|
22
22
|
self.tool = annotator_logic
|
|
23
23
|
self.histories: Dict = {}
|
|
24
24
|
annotator_logic.init(self)
|
|
25
|
+
self.status_sent = False
|
|
25
26
|
|
|
26
27
|
def register_sio_events(self, sio_client: AsyncClient):
|
|
27
28
|
|
|
@@ -50,8 +51,6 @@ class AnnotatorNode(Node):
|
|
|
50
51
|
raise
|
|
51
52
|
|
|
52
53
|
if tool_result.annotation:
|
|
53
|
-
if not self.sio_is_initialized():
|
|
54
|
-
raise Exception('Socket client waas not initialized')
|
|
55
54
|
await self.sio_client.call('update_segmentation_annotation', (user_input.data.context.organization,
|
|
56
55
|
user_input.data.context.project,
|
|
57
56
|
jsonable_encoder(asdict(tool_result.annotation))), timeout=30)
|
|
@@ -67,6 +66,9 @@ class AnnotatorNode(Node):
|
|
|
67
66
|
return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
|
|
68
67
|
|
|
69
68
|
async def send_status(self):
|
|
69
|
+
if self.status_sent:
|
|
70
|
+
return
|
|
71
|
+
|
|
70
72
|
status = AnnotationNodeStatus(
|
|
71
73
|
id=self.uuid,
|
|
72
74
|
name=self.name,
|
|
@@ -75,28 +77,27 @@ class AnnotatorNode(Node):
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
self.log.info(f'Sending status {status}')
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
try:
|
|
81
|
+
result = await self.sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.log.error(f'Error for updating: {str(e)}')
|
|
84
|
+
return
|
|
85
|
+
|
|
81
86
|
assert isinstance(result, Dict)
|
|
82
87
|
response = from_dict(data_class=SocketResponse, data=result)
|
|
83
88
|
|
|
84
89
|
if not response.success:
|
|
85
90
|
self.log.error(f'Error for updating: Response from loop was : {asdict(response)}')
|
|
91
|
+
else:
|
|
92
|
+
self.status_sent = True
|
|
86
93
|
|
|
87
94
|
async def download_image(self, context: Context, uuid: str):
|
|
88
|
-
project_folder =
|
|
95
|
+
project_folder = create_project_folder(context)
|
|
89
96
|
images_folder = create_image_folder(project_folder)
|
|
90
97
|
|
|
91
98
|
downloader = DataExchanger(context=context, loop_communicator=self.loop_communicator)
|
|
92
99
|
await downloader.download_images([uuid], images_folder)
|
|
93
100
|
|
|
94
|
-
async def get_state(self):
|
|
95
|
-
return NodeState.Online
|
|
96
|
-
|
|
97
|
-
def get_node_type(self):
|
|
98
|
-
return 'annotation_node'
|
|
99
|
-
|
|
100
101
|
async def on_startup(self):
|
|
101
102
|
pass
|
|
102
103
|
|
|
@@ -104,4 +105,4 @@ class AnnotatorNode(Node):
|
|
|
104
105
|
pass
|
|
105
106
|
|
|
106
107
|
async def on_repeat(self):
|
|
107
|
-
|
|
108
|
+
await self.send_status()
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
from .annotations import
|
|
2
|
-
|
|
3
|
-
from .detections import (BoxDetection, ClassificationDetection, Detections,
|
|
4
|
-
Observation, Point, PointDetection,
|
|
1
|
+
from .annotations import AnnotationData, AnnotationEventType, SegmentationAnnotation, ToolOutput, UserInput
|
|
2
|
+
from .detections import (BoxDetection, ClassificationDetection, Detections, Observation, Point, PointDetection,
|
|
5
3
|
SegmentationDetection, Shape)
|
|
6
|
-
from .general import (AnnotationNodeStatus, Category, CategoryType, Context,
|
|
7
|
-
|
|
8
|
-
NodeState, NodeStatus)
|
|
4
|
+
from .general import (AnnotationNodeStatus, Category, CategoryType, Context, DetectionStatus, ErrorConfiguration,
|
|
5
|
+
ModelInformation, NodeState, NodeStatus)
|
|
9
6
|
from .socket_response import SocketResponse
|
|
10
|
-
from .training import (
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
from .training import (Errors, Hyperparameter, Model, PretrainedModel, TrainerState, Training, TrainingData,
|
|
8
|
+
TrainingError, TrainingOut, TrainingStateData, TrainingStatus)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'AnnotationData', 'AnnotationEventType', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
|
|
12
|
+
'BoxDetection', 'ClassificationDetection', 'Detections', 'Observation', 'Point', 'PointDetection',
|
|
13
|
+
'SegmentationDetection', 'Shape',
|
|
14
|
+
'AnnotationNodeStatus', 'Category', 'CategoryType', 'Context', 'DetectionStatus', 'ErrorConfiguration',
|
|
15
|
+
'ModelInformation', 'NodeState', 'NodeStatus',
|
|
16
|
+
'SocketResponse',
|
|
17
|
+
'Errors', 'Hyperparameter', 'Model', 'PretrainedModel', 'TrainerState', 'Training', 'TrainingData',
|
|
18
|
+
'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
|
|
19
|
+
]
|
|
@@ -13,8 +13,11 @@ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) e
|
|
|
13
13
|
|
|
14
14
|
@dataclass(**KWONLY_SLOTS)
|
|
15
15
|
class BoxDetection():
|
|
16
|
+
"""Coordinates according to COCO format. x,y is the top left corner of the box.
|
|
17
|
+
x increases to the right, y increases downwards.
|
|
18
|
+
"""
|
|
16
19
|
category_name: str
|
|
17
|
-
x: int
|
|
20
|
+
x: int
|
|
18
21
|
y: int
|
|
19
22
|
width: int
|
|
20
23
|
height: int
|
|
@@ -47,6 +50,8 @@ class BoxDetection():
|
|
|
47
50
|
|
|
48
51
|
@dataclass(**KWONLY_SLOTS)
|
|
49
52
|
class PointDetection():
|
|
53
|
+
"""Coordinates according to COCO format. x,y is the center of the point.
|
|
54
|
+
x increases to the right, y increases downwards."""
|
|
50
55
|
category_name: str
|
|
51
56
|
x: float
|
|
52
57
|
y: float
|
|
@@ -111,7 +116,7 @@ class Detections():
|
|
|
111
116
|
point_detections: List[PointDetection] = field(default_factory=list)
|
|
112
117
|
segmentation_detections: List[SegmentationDetection] = field(default_factory=list)
|
|
113
118
|
classification_detections: List[ClassificationDetection] = field(default_factory=list)
|
|
114
|
-
tags:
|
|
119
|
+
tags: List[str] = field(default_factory=list)
|
|
115
120
|
date: Optional[str] = field(default_factory=current_datetime)
|
|
116
121
|
image_id: Optional[str] = None # used for detection of trainers
|
|
117
122
|
|
|
@@ -34,10 +34,6 @@ class Category():
|
|
|
34
34
|
return [from_dict(data_class=Category, data=value) for value in values]
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def create_category(identifier: str, name: str, ctype: Union[CategoryType, str]): # TODO: This is probably unused
|
|
38
|
-
return Category(id=identifier, name=name, description='', hotkey='', color='', type=ctype, point_size=None)
|
|
39
|
-
|
|
40
|
-
|
|
41
37
|
@dataclass(**KWONLY_SLOTS)
|
|
42
38
|
class Context():
|
|
43
39
|
organization: str
|
|
@@ -57,6 +53,7 @@ class ModelInformation():
|
|
|
57
53
|
categories: List[Category]
|
|
58
54
|
resolution: Optional[int] = None
|
|
59
55
|
model_root_path: Optional[str] = None
|
|
56
|
+
model_size: Optional[str] = None
|
|
60
57
|
|
|
61
58
|
@property
|
|
62
59
|
def context(self):
|
|
@@ -64,6 +61,8 @@ class ModelInformation():
|
|
|
64
61
|
|
|
65
62
|
@staticmethod
|
|
66
63
|
def load_from_disk(model_root_path: str) -> Optional['ModelInformation']:
|
|
64
|
+
"""Load model.json from model_root_path and return ModelInformation object.
|
|
65
|
+
"""
|
|
67
66
|
model_info_file_path = f'{model_root_path}/model.json'
|
|
68
67
|
if not os.path.exists(model_info_file_path):
|
|
69
68
|
logging.warning(f"could not find model information file '{model_info_file_path}'")
|
|
@@ -90,6 +89,10 @@ class ModelInformation():
|
|
|
90
89
|
del self_as_dict['model_root_path']
|
|
91
90
|
f.write(json.dumps(self_as_dict))
|
|
92
91
|
|
|
92
|
+
@staticmethod
|
|
93
|
+
def from_dict(data: Dict) -> 'ModelInformation':
|
|
94
|
+
return from_dict(ModelInformation, data=data)
|
|
95
|
+
|
|
93
96
|
|
|
94
97
|
@dataclass(**KWONLY_SLOTS)
|
|
95
98
|
class ErrorConfiguration():
|
|
@@ -117,7 +120,7 @@ class NodeState(str, Enum):
|
|
|
117
120
|
class NodeStatus():
|
|
118
121
|
id: str
|
|
119
122
|
name: str
|
|
120
|
-
state: Optional[NodeState] = NodeState.
|
|
123
|
+
state: Optional[NodeState] = NodeState.Online
|
|
121
124
|
uptime: Optional[int] = 0
|
|
122
125
|
errors: Dict = field(default_factory=dict)
|
|
123
126
|
capabilities: List[str] = field(default_factory=list)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import sys
|
|
3
|
+
import time
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from enum import Enum
|
|
5
|
-
from
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional
|
|
6
8
|
|
|
7
9
|
# pylint: disable=no-name-in-module
|
|
8
10
|
from .general import Category, Context
|
|
@@ -16,6 +18,14 @@ class Hyperparameter():
|
|
|
16
18
|
flip_rl: bool
|
|
17
19
|
flip_ud: bool
|
|
18
20
|
|
|
21
|
+
@staticmethod
|
|
22
|
+
def from_data(data: Dict):
|
|
23
|
+
return Hyperparameter(
|
|
24
|
+
resolution=data['resolution'],
|
|
25
|
+
flip_rl=data.get('flip_rl', False),
|
|
26
|
+
flip_ud=data.get('flip_ud', False)
|
|
27
|
+
)
|
|
28
|
+
|
|
19
29
|
|
|
20
30
|
@dataclass(**KWONLY_SLOTS)
|
|
21
31
|
class TrainingData():
|
|
@@ -41,14 +51,15 @@ class PretrainedModel():
|
|
|
41
51
|
description: str
|
|
42
52
|
|
|
43
53
|
|
|
44
|
-
class
|
|
54
|
+
class TrainerState(str, Enum):
|
|
55
|
+
Idle = 'idle'
|
|
45
56
|
Initialized = 'initialized'
|
|
46
57
|
Preparing = 'preparing'
|
|
47
58
|
DataDownloading = 'data_downloading'
|
|
48
59
|
DataDownloaded = 'data_downloaded'
|
|
49
60
|
TrainModelDownloading = 'train_model_downloading'
|
|
50
61
|
TrainModelDownloaded = 'train_model_downloaded'
|
|
51
|
-
TrainingRunning = '
|
|
62
|
+
TrainingRunning = 'running'
|
|
52
63
|
TrainingFinished = 'training_finished'
|
|
53
64
|
ConfusionMatrixSyncing = 'confusion_matrix_syncing'
|
|
54
65
|
ConfusionMatrixSynced = 'confusion_matrix_synced'
|
|
@@ -62,9 +73,9 @@ class TrainingState(str, Enum):
|
|
|
62
73
|
|
|
63
74
|
@dataclass(**KWONLY_SLOTS)
|
|
64
75
|
class TrainingStatus():
|
|
65
|
-
id: str #
|
|
76
|
+
id: str # NOTE this must not be changed, but tests wont detect a change -> update tests!
|
|
66
77
|
name: str
|
|
67
|
-
state:
|
|
78
|
+
state: Optional[str]
|
|
68
79
|
errors: Optional[Dict]
|
|
69
80
|
uptime: Optional[float]
|
|
70
81
|
progress: Optional[float]
|
|
@@ -77,13 +88,13 @@ class TrainingStatus():
|
|
|
77
88
|
architecture: Optional[str] = None
|
|
78
89
|
context: Optional[Context] = None
|
|
79
90
|
|
|
80
|
-
def short_str(self):
|
|
91
|
+
def short_str(self) -> str:
|
|
81
92
|
prgr = f'{self.progress * 100:.0f}%' if self.progress else ''
|
|
82
93
|
trtesk = f'{self.train_image_count}/{self.test_image_count}/{self.skipped_image_count}' if self.train_image_count else 'n.a.'
|
|
83
94
|
cntxt = f'{self.context.organization}/{self.context.project}' if self.context else ''
|
|
84
95
|
hyps = f'({self.hyperparameters})' if self.hyperparameters else ''
|
|
85
96
|
arch = f'.{self.architecture} - ' if self.architecture else ''
|
|
86
|
-
return f'[{str(self.state)} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
|
|
97
|
+
return f'[{str(self.state).rsplit(".", maxsplit=1)[-1]} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
|
|
87
98
|
|
|
88
99
|
|
|
89
100
|
@dataclass(**KWONLY_SLOTS)
|
|
@@ -91,21 +102,35 @@ class Training():
|
|
|
91
102
|
id: str
|
|
92
103
|
context: Context
|
|
93
104
|
|
|
94
|
-
project_folder: str
|
|
95
|
-
images_folder: str
|
|
96
|
-
training_folder: str
|
|
105
|
+
project_folder: str # f'{GLOBALS.data_folder}/{context.organization}/{context.project}'
|
|
106
|
+
images_folder: str # f'{project_folder}/images'
|
|
107
|
+
training_folder: str # f'{project_folder}/trainings/{trainings_id}'
|
|
108
|
+
start_time: float = field(default_factory=time.time)
|
|
109
|
+
|
|
110
|
+
# model uuid to download (to continue training) | is not a uuid when training from scratch (blank or pt-name from provided_pretrained_models->name)
|
|
111
|
+
base_model_uuid_or_name: Optional[str] = None
|
|
97
112
|
|
|
98
|
-
base_model_id: Optional[str] = None
|
|
99
113
|
data: Optional[TrainingData] = None
|
|
100
114
|
training_number: Optional[int] = None
|
|
101
|
-
training_state: Optional[
|
|
102
|
-
|
|
115
|
+
training_state: Optional[str] = None
|
|
116
|
+
model_uuid_for_detecting: Optional[str] = None
|
|
103
117
|
hyperparameters: Optional[Dict] = None
|
|
104
118
|
|
|
119
|
+
@property
|
|
120
|
+
def training_folder_path(self) -> Path:
|
|
121
|
+
return Path(self.training_folder)
|
|
122
|
+
|
|
123
|
+
def set_values_from_data(self, data: Dict) -> None:
|
|
124
|
+
self.data = TrainingData(categories=Category.from_list(data['categories']))
|
|
125
|
+
self.data.hyperparameter = Hyperparameter.from_data(data=data)
|
|
126
|
+
self.training_number = data['training_number']
|
|
127
|
+
self.base_model_uuid_or_name = data['id']
|
|
128
|
+
self.training_state = TrainerState.Initialized
|
|
129
|
+
|
|
105
130
|
|
|
106
131
|
@dataclass(**KWONLY_SLOTS)
|
|
107
132
|
class TrainingOut():
|
|
108
|
-
confusion_matrix: Optional[Dict] = None
|
|
133
|
+
confusion_matrix: Optional[Dict] = None # This is actually just class-wise metrics
|
|
109
134
|
train_image_count: Optional[int] = None
|
|
110
135
|
test_image_count: Optional[int] = None
|
|
111
136
|
trainer_id: Optional[str] = None
|
|
@@ -113,9 +138,9 @@ class TrainingOut():
|
|
|
113
138
|
|
|
114
139
|
|
|
115
140
|
@dataclass(**KWONLY_SLOTS)
|
|
116
|
-
class
|
|
117
|
-
confusion_matrix:
|
|
118
|
-
meta_information:
|
|
141
|
+
class TrainingStateData():
|
|
142
|
+
confusion_matrix: Dict = field(default_factory=dict)
|
|
143
|
+
meta_information: Dict = field(default_factory=dict)
|
|
119
144
|
|
|
120
145
|
|
|
121
146
|
@dataclass(**KWONLY_SLOTS)
|
|
@@ -130,8 +155,8 @@ class Model():
|
|
|
130
155
|
|
|
131
156
|
|
|
132
157
|
class Errors():
|
|
133
|
-
def __init__(self):
|
|
134
|
-
self._errors: Dict = {}
|
|
158
|
+
def __init__(self) -> None:
|
|
159
|
+
self._errors: Dict[str, str] = {}
|
|
135
160
|
|
|
136
161
|
def set(self, key: str, value: str):
|
|
137
162
|
self._errors[key] = value
|
|
@@ -140,7 +165,7 @@ class Errors():
|
|
|
140
165
|
def errors(self) -> Dict:
|
|
141
166
|
return self._errors
|
|
142
167
|
|
|
143
|
-
def reset(self, key: str):
|
|
168
|
+
def reset(self, key: str) -> None:
|
|
144
169
|
try:
|
|
145
170
|
del self._errors[key]
|
|
146
171
|
except AttributeError:
|
|
@@ -148,7 +173,7 @@ class Errors():
|
|
|
148
173
|
except KeyError:
|
|
149
174
|
pass
|
|
150
175
|
|
|
151
|
-
def reset_all(self):
|
|
176
|
+
def reset_all(self) -> None:
|
|
152
177
|
self._errors = {}
|
|
153
178
|
|
|
154
179
|
def has_error_for(self, key: str) -> bool:
|
|
@@ -162,3 +187,6 @@ class TrainingError(Exception):
|
|
|
162
187
|
def __init__(self, cause: str, *args: object) -> None:
|
|
163
188
|
super().__init__(*args)
|
|
164
189
|
self.cause = cause
|
|
190
|
+
|
|
191
|
+
def __str__(self) -> str:
|
|
192
|
+
return f'TrainingError: {self.cause}'
|