learning-loop-node 0.10.5__tar.gz → 0.10.7__tar.gz
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-0.10.5 → learning_loop_node-0.10.7}/PKG-INFO +23 -1
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/README.md +22 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/annotation/tests/test_annotator_node.py +6 -6
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_exchanger.py +9 -4
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/detector_node.py +4 -7
- learning_loop_node-0.10.7/learning_loop_node/detector/outbox.py +185 -0
- learning_loop_node-0.10.7/learning_loop_node/detector/rest/outbox_mode.py +35 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/upload.py +6 -2
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/test_client_communication.py +16 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/test_outbox.py +23 -5
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_executor.py +1 -0
- learning_loop_node-0.10.7/learning_loop_node/trainer/exceptions.py +2 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/executor.py +3 -3
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/conftest.py +2 -2
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_detecting.py +4 -4
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_upload_model.py +4 -5
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/testing_trainer_logic.py +1 -1
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/trainer_logic_generic.py +53 -32
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/pyproject.toml +1 -1
- learning_loop_node-0.10.5/learning_loop_node/detector/outbox.py +0 -117
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/conftest.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/detections.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/detector_logic.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/tests/test_observation.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/tests/test_relevance_group.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/inbox_filter/tests/test_unexpected_observations_count.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/detect.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/conftest.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/test.jpg +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/test_relevance_filter.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/tests/testing_detector.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/examples/novelty_score_updater.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/globals.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/helpers/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/helpers/misc.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/node.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/pytest.ini +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/conftest.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_data/file_1.txt +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_data/file_2.txt +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_data/model.json +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_data_classes.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_downloader.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_helper.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_learning_loop_node.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/io_helpers.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/rest/controls.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/state_helper.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/__init__.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_cleanup.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_download_train_model.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_prepare.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_sync_confusion_matrix.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_train.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/states/test_state_upload_detections.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/test_errors.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/test_trainer_states.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/trainer_logic.py +0 -0
- {learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/trainer_node.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: learning-loop-node
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.7
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Home-page: https://github.com/zauberzeug/learning_loop_node
|
|
6
6
|
License: MIT
|
|
@@ -81,6 +81,8 @@ from learning_loop_node/learning_loop_node
|
|
|
81
81
|
|
|
82
82
|
Detector Nodes are normally deployed on edge devices like robots or machinery but can also run in the cloud to provide backend services for an app or similar. These nodes register themself at the Learning Loop. They provide REST and Socket.io APIs to run inference on images. The processed images can automatically be used for active learning: e.g. uncertain predictions will be send to the Learning Loop.
|
|
83
83
|
|
|
84
|
+
### Running Inference
|
|
85
|
+
|
|
84
86
|
Images can be send to the detector node via socketio or rest.
|
|
85
87
|
The later approach can be used via curl,
|
|
86
88
|
|
|
@@ -102,6 +104,26 @@ The detector also has a sio **upload endpoint** that can be used to upload image
|
|
|
102
104
|
|
|
103
105
|
The endpoint returns None if the upload was successful and an error message otherwise.
|
|
104
106
|
|
|
107
|
+
### Changing the outbox mode
|
|
108
|
+
|
|
109
|
+
If the autoupload is set to `all` or `filtered` (selected) images and the corresponding detections are saved on HDD (the outbox). A background thread will upload the images and detections to the Learning Loop. The outbox is located in the `outbox` folder in the root directory of the node. The outbox can be cleared by deleting the files in the folder.
|
|
110
|
+
|
|
111
|
+
The continuous upload can be stopped/started via a REST enpoint:
|
|
112
|
+
|
|
113
|
+
Example Usage:
|
|
114
|
+
|
|
115
|
+
- Enable upload: `curl -X PUT -d "continuous_upload" http://localhost/outbox_mode`
|
|
116
|
+
- Disable upload: `curl -X PUT -d "stopped" http://localhost/outbox_mode`
|
|
117
|
+
|
|
118
|
+
The current state can be queried via a GET request:
|
|
119
|
+
`curl http://localhost/outbox_mode`
|
|
120
|
+
|
|
121
|
+
### Explicit upload
|
|
122
|
+
|
|
123
|
+
The detector has a REST endpoint to upload images (and detections) to the Learning Loop. The endpoint takes a POST request with the image and optionally the detections. The image is expected to be in jpg format. The detections are expected to be a json dictionary. Example:
|
|
124
|
+
|
|
125
|
+
`curl -X POST -F 'files=@test.jpg' "http://localhost:/upload"`
|
|
126
|
+
|
|
105
127
|
## Trainer Node
|
|
106
128
|
|
|
107
129
|
Trainers fetch the images and anntoations from the Learning Loop to train new models.
|
|
@@ -41,6 +41,8 @@ from learning_loop_node/learning_loop_node
|
|
|
41
41
|
|
|
42
42
|
Detector Nodes are normally deployed on edge devices like robots or machinery but can also run in the cloud to provide backend services for an app or similar. These nodes register themself at the Learning Loop. They provide REST and Socket.io APIs to run inference on images. The processed images can automatically be used for active learning: e.g. uncertain predictions will be send to the Learning Loop.
|
|
43
43
|
|
|
44
|
+
### Running Inference
|
|
45
|
+
|
|
44
46
|
Images can be send to the detector node via socketio or rest.
|
|
45
47
|
The later approach can be used via curl,
|
|
46
48
|
|
|
@@ -62,6 +64,26 @@ The detector also has a sio **upload endpoint** that can be used to upload image
|
|
|
62
64
|
|
|
63
65
|
The endpoint returns None if the upload was successful and an error message otherwise.
|
|
64
66
|
|
|
67
|
+
### Changing the outbox mode
|
|
68
|
+
|
|
69
|
+
If the autoupload is set to `all` or `filtered` (selected) images and the corresponding detections are saved on HDD (the outbox). A background thread will upload the images and detections to the Learning Loop. The outbox is located in the `outbox` folder in the root directory of the node. The outbox can be cleared by deleting the files in the folder.
|
|
70
|
+
|
|
71
|
+
The continuous upload can be stopped/started via a REST enpoint:
|
|
72
|
+
|
|
73
|
+
Example Usage:
|
|
74
|
+
|
|
75
|
+
- Enable upload: `curl -X PUT -d "continuous_upload" http://localhost/outbox_mode`
|
|
76
|
+
- Disable upload: `curl -X PUT -d "stopped" http://localhost/outbox_mode`
|
|
77
|
+
|
|
78
|
+
The current state can be queried via a GET request:
|
|
79
|
+
`curl http://localhost/outbox_mode`
|
|
80
|
+
|
|
81
|
+
### Explicit upload
|
|
82
|
+
|
|
83
|
+
The detector has a REST endpoint to upload images (and detections) to the Learning Loop. The endpoint takes a POST request with the image and optionally the detections. The image is expected to be in jpg format. The detections are expected to be a json dictionary. Example:
|
|
84
|
+
|
|
85
|
+
`curl -X POST -F 'files=@test.jpg' "http://localhost:/upload"`
|
|
86
|
+
|
|
65
87
|
## Trainer Node
|
|
66
88
|
|
|
67
89
|
Trainers fetch the images and anntoations from the Learning Loop to train new models.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import os
|
|
2
3
|
from dataclasses import asdict
|
|
3
4
|
from typing import Dict
|
|
@@ -7,10 +8,8 @@ from fastapi.encoders import jsonable_encoder
|
|
|
7
8
|
|
|
8
9
|
from learning_loop_node.annotation.annotator_logic import AnnotatorLogic
|
|
9
10
|
from learning_loop_node.annotation.annotator_node import AnnotatorNode
|
|
10
|
-
from learning_loop_node.data_classes import (AnnotationData,
|
|
11
|
-
|
|
12
|
-
CategoryType, Context, Point,
|
|
13
|
-
ToolOutput, UserInput)
|
|
11
|
+
from learning_loop_node.data_classes import (AnnotationData, AnnotationEventType, Category, CategoryType, Context,
|
|
12
|
+
Point, ToolOutput, UserInput)
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class MockedAnnotatatorLogic(AnnotatorLogic):
|
|
@@ -29,7 +28,7 @@ def default_user_input() -> UserInput:
|
|
|
29
28
|
coordinate=Point(x=0, y=0),
|
|
30
29
|
event_type=AnnotationEventType.LeftMouseDown,
|
|
31
30
|
context=Context(organization='zauberzeug', project='pytest_p'),
|
|
32
|
-
image_uuid='
|
|
31
|
+
image_uuid='f786350c-89ca-9424-9b00-720a9a85fe09',
|
|
33
32
|
category=Category(id='some_id', name='category_1', description='',
|
|
34
33
|
hotkey='', color='', type=CategoryType.Segmentation)
|
|
35
34
|
)
|
|
@@ -40,7 +39,8 @@ def default_user_input() -> UserInput:
|
|
|
40
39
|
|
|
41
40
|
@pytest.mark.asyncio
|
|
42
41
|
async def test_image_download(setup_test_project): # pylint: disable=unused-argument
|
|
43
|
-
|
|
42
|
+
# TODO: This test depends on a pseudo-random uuid..
|
|
43
|
+
image_path = '/tmp/learning_loop_lib_data/zauberzeug/pytest_p/images/f786350c-89ca-9424-9b00-720a9a85fe09.jpg'
|
|
44
44
|
|
|
45
45
|
assert os.path.exists(image_path) is False
|
|
46
46
|
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/data_exchanger.py
RENAMED
|
@@ -14,6 +14,7 @@ import aiofiles # type: ignore
|
|
|
14
14
|
from .data_classes import Context
|
|
15
15
|
from .helpers.misc import create_resource_paths, create_task, is_valid_image
|
|
16
16
|
from .loop_communication import LoopCommunicator
|
|
17
|
+
from .trainer.exceptions import CriticalError
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class DownloadError(Exception):
|
|
@@ -159,13 +160,17 @@ class DataExchanger():
|
|
|
159
160
|
logging.info(f'Downloaded model {model_uuid}({model_format}) to {target_folder}.')
|
|
160
161
|
return created_files
|
|
161
162
|
|
|
162
|
-
async def upload_model_get_uuid(self, context: Context, files: List[str], training_number: Optional[int], mformat: str) ->
|
|
163
|
-
"""Used by the trainers. Function returns the new model uuid to use for detection.
|
|
163
|
+
async def upload_model_get_uuid(self, context: Context, files: List[str], training_number: Optional[int], mformat: str) -> str:
|
|
164
|
+
"""Used by the trainers. Function returns the new model uuid to use for detection.
|
|
165
|
+
|
|
166
|
+
:return: The new model uuid.
|
|
167
|
+
:raise CriticalError: If the upload does not return status code 200.
|
|
168
|
+
"""
|
|
164
169
|
response = await self.loop_communicator.put(f'/{context.organization}/projects/{context.project}/trainings/{training_number}/models/latest/{mformat}/file', files=files)
|
|
165
170
|
if response.status_code != 200:
|
|
166
171
|
logging.error(f'Could not upload model for training {training_number}, format {mformat}: {response.text}')
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
raise CriticalError(
|
|
173
|
+
f'Could not upload model for training {training_number}, format {mformat}: {response.text}')
|
|
169
174
|
|
|
170
175
|
uploaded_model = response.json()
|
|
171
176
|
logging.info(f'Uploaded model for training {training_number}, format {mformat}. Response is: {uploaded_model}')
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/detector_node.py
RENAMED
|
@@ -27,6 +27,7 @@ from .rest import about as rest_about
|
|
|
27
27
|
from .rest import backdoor_controls
|
|
28
28
|
from .rest import detect as rest_detect
|
|
29
29
|
from .rest import operation_mode as rest_mode
|
|
30
|
+
from .rest import outbox_mode as rest_outbox_mode
|
|
30
31
|
from .rest import upload as rest_upload
|
|
31
32
|
from .rest.operation_mode import OperationMode
|
|
32
33
|
|
|
@@ -57,6 +58,7 @@ class DetectorNode(Node):
|
|
|
57
58
|
self.include_router(rest_upload.router, prefix="")
|
|
58
59
|
self.include_router(rest_mode.router, tags=["operation_mode"])
|
|
59
60
|
self.include_router(rest_about.router, tags=["about"])
|
|
61
|
+
self.include_router(rest_outbox_mode.router, tags=["outbox_mode"])
|
|
60
62
|
|
|
61
63
|
if use_backdoor_controls:
|
|
62
64
|
self.include_router(backdoor_controls.router)
|
|
@@ -89,7 +91,7 @@ class DetectorNode(Node):
|
|
|
89
91
|
|
|
90
92
|
async def on_startup(self) -> None:
|
|
91
93
|
try:
|
|
92
|
-
self.outbox.
|
|
94
|
+
self.outbox.ensure_continuous_upload()
|
|
93
95
|
self.detector_logic.load_model()
|
|
94
96
|
except Exception:
|
|
95
97
|
self.log.exception("error during 'startup'")
|
|
@@ -97,7 +99,7 @@ class DetectorNode(Node):
|
|
|
97
99
|
|
|
98
100
|
async def on_shutdown(self) -> None:
|
|
99
101
|
try:
|
|
100
|
-
self.outbox.
|
|
102
|
+
self.outbox.ensure_continuous_upload_stopped()
|
|
101
103
|
for sid in self.connected_clients:
|
|
102
104
|
# pylint: disable=no-member
|
|
103
105
|
await self.sio.disconnect(sid) # type:ignore
|
|
@@ -156,9 +158,6 @@ class DetectorNode(Node):
|
|
|
156
158
|
|
|
157
159
|
tags = data.get('tags', [])
|
|
158
160
|
tags.append('picked_by_system')
|
|
159
|
-
camera_id = data.get('camera-id', None) or data.get('mac', None)
|
|
160
|
-
if camera_id is not None:
|
|
161
|
-
tags.append(camera_id)
|
|
162
161
|
|
|
163
162
|
loop = asyncio.get_event_loop()
|
|
164
163
|
try:
|
|
@@ -315,8 +314,6 @@ class DetectorNode(Node):
|
|
|
315
314
|
n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
|
|
316
315
|
self.log.info(f'detected:{n_bo} boxes, {n_po} points, {n_se} segs, {n_cl} classes')
|
|
317
316
|
|
|
318
|
-
if camera_id is not None:
|
|
319
|
-
tags.append(camera_id)
|
|
320
317
|
if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
|
|
321
318
|
Thread(target=self.relevance_filter.may_upload_detections,
|
|
322
319
|
args=(detections, camera_id, raw_image, tags)).start()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from glob import glob
|
|
10
|
+
from io import BufferedReader, TextIOWrapper
|
|
11
|
+
from multiprocessing import Event
|
|
12
|
+
from multiprocessing.synchronize import Event as SyncEvent
|
|
13
|
+
from threading import Thread
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
from fastapi.encoders import jsonable_encoder
|
|
18
|
+
|
|
19
|
+
from ..data_classes import Detections
|
|
20
|
+
from ..globals import GLOBALS
|
|
21
|
+
from ..helpers import environment_reader
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OutboxMode(Enum):
|
|
25
|
+
CONTINUOUS_UPLOAD = 'continuous_upload'
|
|
26
|
+
STOPPED = 'stopped'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Outbox():
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.log = logging.getLogger()
|
|
32
|
+
self.path = f'{GLOBALS.data_folder}/outbox'
|
|
33
|
+
os.makedirs(self.path, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
self.log = logging.getLogger()
|
|
36
|
+
host = environment_reader.host()
|
|
37
|
+
o = environment_reader.organization()
|
|
38
|
+
p = environment_reader.project()
|
|
39
|
+
|
|
40
|
+
assert o and p, 'Outbox needs an organization and a project '
|
|
41
|
+
base_url = f'http{"s" if "learning-loop.ai" in host else ""}://{host}/api'
|
|
42
|
+
base: str = base_url
|
|
43
|
+
self.target_uri = f'{base}/{o}/projects/{p}/images'
|
|
44
|
+
self.log.info('Outbox initialized with target_uri: %s', self.target_uri)
|
|
45
|
+
|
|
46
|
+
self.BATCH_SIZE = 20
|
|
47
|
+
self.UPLOAD_TIMEOUT_S = 30
|
|
48
|
+
|
|
49
|
+
self.shutdown_event: SyncEvent = Event()
|
|
50
|
+
self.upload_process: Optional[Thread] = None
|
|
51
|
+
|
|
52
|
+
def save(self, image: bytes, detections: Optional[Detections] = None, tags: Optional[List[str]] = None) -> None:
|
|
53
|
+
if detections is None:
|
|
54
|
+
detections = Detections()
|
|
55
|
+
if not tags:
|
|
56
|
+
tags = []
|
|
57
|
+
identifier = datetime.now().isoformat(sep='_', timespec='milliseconds')
|
|
58
|
+
tmp = f'{GLOBALS.data_folder}/tmp/{identifier}'
|
|
59
|
+
detections.tags = tags
|
|
60
|
+
detections.date = identifier
|
|
61
|
+
os.makedirs(tmp, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
with open(tmp + '/image.json', 'w') as f:
|
|
64
|
+
json.dump(jsonable_encoder(asdict(detections)), f)
|
|
65
|
+
|
|
66
|
+
with open(tmp + '/image.jpg', 'wb') as f:
|
|
67
|
+
f.write(image)
|
|
68
|
+
|
|
69
|
+
if os.path.exists(tmp):
|
|
70
|
+
os.rename(tmp, self.path + '/' + identifier) # NOTE rename is atomic so upload can run in parallel
|
|
71
|
+
else:
|
|
72
|
+
self.log.error('Could not rename %s to %s', tmp, self.path + '/' + identifier)
|
|
73
|
+
|
|
74
|
+
def get_data_files(self):
|
|
75
|
+
return glob(f'{self.path}/*')
|
|
76
|
+
|
|
77
|
+
def ensure_continuous_upload(self):
|
|
78
|
+
self.log.debug('start_continuous_upload')
|
|
79
|
+
if self._upload_process_alive():
|
|
80
|
+
self.log.debug('Upload thread already running')
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
self.shutdown_event.clear()
|
|
84
|
+
self.upload_process = Thread(target=self._continuous_upload, name='OutboxUpload')
|
|
85
|
+
self.upload_process.start()
|
|
86
|
+
|
|
87
|
+
def _continuous_upload(self):
|
|
88
|
+
self.log.info('continuous upload started')
|
|
89
|
+
assert self.shutdown_event is not None
|
|
90
|
+
while not self.shutdown_event.is_set():
|
|
91
|
+
self.upload()
|
|
92
|
+
time.sleep(5)
|
|
93
|
+
self.log.info('continuous upload ended')
|
|
94
|
+
|
|
95
|
+
def upload(self):
|
|
96
|
+
items = self.get_data_files()
|
|
97
|
+
if items:
|
|
98
|
+
self.log.info('Found %s images to upload', len(items))
|
|
99
|
+
for i in range(0, len(items), self.BATCH_SIZE):
|
|
100
|
+
batch_items = items[i:i+self.BATCH_SIZE]
|
|
101
|
+
if self.shutdown_event.is_set():
|
|
102
|
+
break
|
|
103
|
+
try:
|
|
104
|
+
self._upload_batch(batch_items)
|
|
105
|
+
except Exception:
|
|
106
|
+
self.log.exception('Could not upload files')
|
|
107
|
+
else:
|
|
108
|
+
self.log.info('No images found to upload')
|
|
109
|
+
|
|
110
|
+
def _upload_batch(self, items: List[str]):
|
|
111
|
+
data: List[tuple[str, TextIOWrapper | BufferedReader]] = []
|
|
112
|
+
data = [('files', open(f'{item}/image.json', 'r')) for item in items]
|
|
113
|
+
data += [('files', open(f'{item}/image.jpg', 'rb')) for item in items]
|
|
114
|
+
|
|
115
|
+
response = requests.post(self.target_uri, files=data, timeout=self.UPLOAD_TIMEOUT_S)
|
|
116
|
+
if response.status_code == 200:
|
|
117
|
+
for item in items:
|
|
118
|
+
shutil.rmtree(item, ignore_errors=True)
|
|
119
|
+
self.log.info('Uploaded %s images successfully', len(items))
|
|
120
|
+
elif response.status_code == 422:
|
|
121
|
+
if len(items) == 1:
|
|
122
|
+
self.log.error('Broken content in image: %s\n Skipping.', items[0])
|
|
123
|
+
shutil.rmtree(items[0], ignore_errors=True)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self.log.exception('Broken content in batch. Splitting and retrying')
|
|
127
|
+
self._upload_batch(items[:len(items)//2])
|
|
128
|
+
self._upload_batch(items[len(items)//2:])
|
|
129
|
+
else:
|
|
130
|
+
self.log.error('Could not upload images: %s', response.content)
|
|
131
|
+
|
|
132
|
+
def ensure_continuous_upload_stopped(self) -> bool:
|
|
133
|
+
self.log.debug('Outbox: Ensuring continuous upload')
|
|
134
|
+
if not self._upload_process_alive():
|
|
135
|
+
self.log.debug('Upload thread already stopped')
|
|
136
|
+
return True
|
|
137
|
+
proc = self.upload_process
|
|
138
|
+
if not proc:
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
assert self.shutdown_event is not None
|
|
143
|
+
self.shutdown_event.set()
|
|
144
|
+
assert proc is not None
|
|
145
|
+
proc.join(self.UPLOAD_TIMEOUT_S + 1)
|
|
146
|
+
except Exception:
|
|
147
|
+
self.log.exception('Error while shutting down upload thread: ')
|
|
148
|
+
|
|
149
|
+
if proc.is_alive():
|
|
150
|
+
self.log.error('Upload thread did not terminate')
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
self.log.info('Upload thread terminated')
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def _upload_process_alive(self) -> bool:
|
|
157
|
+
return bool(self.upload_process and self.upload_process.is_alive())
|
|
158
|
+
|
|
159
|
+
def get_mode(self) -> OutboxMode:
|
|
160
|
+
''':return: current mode ('continuous_upload' or 'stopped')'''
|
|
161
|
+
if self.upload_process and self.upload_process.is_alive():
|
|
162
|
+
current_mode = OutboxMode.CONTINUOUS_UPLOAD
|
|
163
|
+
else:
|
|
164
|
+
current_mode = OutboxMode.STOPPED
|
|
165
|
+
|
|
166
|
+
self.log.debug('Outbox: Current mode is %s', current_mode)
|
|
167
|
+
return current_mode
|
|
168
|
+
|
|
169
|
+
def set_mode(self, mode: OutboxMode | str):
|
|
170
|
+
''':param mode: 'continuous_upload' or 'stopped'
|
|
171
|
+
:raises ValueError: if mode is not a valid OutboxMode
|
|
172
|
+
:raises TimeoutError: if the upload thread does not terminate within 31 seconds with mode='stopped'
|
|
173
|
+
'''
|
|
174
|
+
if isinstance(mode, str):
|
|
175
|
+
mode = OutboxMode(mode)
|
|
176
|
+
|
|
177
|
+
if mode == OutboxMode.CONTINUOUS_UPLOAD:
|
|
178
|
+
self.ensure_continuous_upload()
|
|
179
|
+
elif mode == OutboxMode.STOPPED:
|
|
180
|
+
try:
|
|
181
|
+
self.ensure_continuous_upload_stopped()
|
|
182
|
+
except TimeoutError as e:
|
|
183
|
+
raise TimeoutError(f'Upload thread did not terminate within {self.UPLOAD_TIMEOUT_S} seconds.') from e
|
|
184
|
+
|
|
185
|
+
self.log.debug('set outbox mode to %s', mode)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
2
|
+
from fastapi.responses import PlainTextResponse
|
|
3
|
+
|
|
4
|
+
from ..outbox import Outbox
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/outbox_mode")
|
|
10
|
+
async def get_outbox_mode(request: Request):
|
|
11
|
+
'''
|
|
12
|
+
Example Usage
|
|
13
|
+
curl http://localhost/outbox_mode
|
|
14
|
+
'''
|
|
15
|
+
outbox: Outbox = request.app.outbox
|
|
16
|
+
return PlainTextResponse(outbox.get_mode().value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.put("/outbox_mode")
|
|
20
|
+
async def put_outbox_mode(request: Request):
|
|
21
|
+
'''
|
|
22
|
+
Example Usage
|
|
23
|
+
curl -X PUT -d "continuous_upload" http://localhost/outbox_mode
|
|
24
|
+
curl -X PUT -d "stopped" http://localhost/outbox_mode
|
|
25
|
+
'''
|
|
26
|
+
outbox: Outbox = request.app.outbox
|
|
27
|
+
content = str(await request.body(), 'utf-8')
|
|
28
|
+
try:
|
|
29
|
+
outbox.set_mode(content)
|
|
30
|
+
except TimeoutError as e:
|
|
31
|
+
raise HTTPException(202, 'Setting has not completed, yet: ' + str(e)) from e
|
|
32
|
+
except ValueError as e:
|
|
33
|
+
raise HTTPException(422, 'Could not set outbox mode: ' + str(e)) from e
|
|
34
|
+
|
|
35
|
+
return "OK"
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/detector/rest/upload.py
RENAMED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import TYPE_CHECKING, List
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, File, Request, UploadFile
|
|
4
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..detector_node import DetectorNode
|
|
7
|
+
|
|
5
8
|
router = APIRouter()
|
|
6
9
|
|
|
7
10
|
|
|
@@ -13,5 +16,6 @@ async def upload_image(request: Request, files: List[UploadFile] = File(...)):
|
|
|
13
16
|
curl -X POST -F 'files=@test.jpg' "http://localhost:/upload"
|
|
14
17
|
"""
|
|
15
18
|
raw_files = [await file.read() for file in files]
|
|
16
|
-
|
|
19
|
+
node: DetectorNode = request.app
|
|
20
|
+
await node.upload_images(raw_files)
|
|
17
21
|
return 200, "OK"
|
|
@@ -102,3 +102,19 @@ async def test_about_endpoint(test_detector_node: DetectorNode):
|
|
|
102
102
|
assert response_dict['state'] == 'online'
|
|
103
103
|
assert response_dict['target_model'] == '1.1'
|
|
104
104
|
assert any(c.name == 'purple point' for c in model_information.categories)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def test_rest_outbox_mode(test_detector_node: DetectorNode):
|
|
108
|
+
await asyncio.sleep(3)
|
|
109
|
+
|
|
110
|
+
def check_switch_to_mode(mode: str):
|
|
111
|
+
response = requests.put(f'http://localhost:{GLOBALS.detector_port}/outbox_mode',
|
|
112
|
+
data=mode, timeout=30)
|
|
113
|
+
assert response.status_code == 200
|
|
114
|
+
response = requests.get(f'http://localhost:{GLOBALS.detector_port}/outbox_mode', timeout=30)
|
|
115
|
+
assert response.status_code == 200
|
|
116
|
+
assert response.content == mode.encode()
|
|
117
|
+
|
|
118
|
+
check_switch_to_mode('stopped')
|
|
119
|
+
check_switch_to_mode('continuous_upload')
|
|
120
|
+
check_switch_to_mode('stopped')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
from time import sleep
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pytest
|
|
@@ -21,6 +22,7 @@ def test_outbox():
|
|
|
21
22
|
os.mkdir(test_outbox.path)
|
|
22
23
|
|
|
23
24
|
yield test_outbox
|
|
25
|
+
test_outbox.set_mode('stopped')
|
|
24
26
|
shutil.rmtree(test_outbox.path, ignore_errors=True)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -52,11 +54,7 @@ def test_saving_opencv_image(test_outbox: Outbox):
|
|
|
52
54
|
|
|
53
55
|
def test_saving_binary(test_outbox: Outbox):
|
|
54
56
|
assert len(test_outbox.get_data_files()) == 0
|
|
55
|
-
|
|
56
|
-
img.save('/tmp/image.jpg')
|
|
57
|
-
with open('/tmp/image.jpg', 'rb') as f:
|
|
58
|
-
data = f.read()
|
|
59
|
-
test_outbox.save(data)
|
|
57
|
+
save_test_image_to_outbox(test_outbox)
|
|
60
58
|
assert len(test_outbox.get_data_files()) == 1
|
|
61
59
|
|
|
62
60
|
|
|
@@ -66,3 +64,23 @@ async def test_files_are_automatically_uploaded(test_detector_node: DetectorNode
|
|
|
66
64
|
assert len(test_detector_node.outbox.get_data_files()) == 1
|
|
67
65
|
|
|
68
66
|
assert len(test_detector_node.outbox.get_data_files()) == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_set_outbox_mode(test_outbox: Outbox):
|
|
70
|
+
test_outbox.set_mode('stopped')
|
|
71
|
+
save_test_image_to_outbox(outbox=test_outbox)
|
|
72
|
+
sleep(6)
|
|
73
|
+
assert len(test_outbox.get_data_files()) == 1, 'File was cleared even though outbox should be stopped'
|
|
74
|
+
test_outbox.set_mode('continuous_upload')
|
|
75
|
+
sleep(6)
|
|
76
|
+
assert len(test_outbox.get_data_files()) == 0, 'File was not cleared even though outbox should be in continuous_upload'
|
|
77
|
+
|
|
78
|
+
### Helper functions ###
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_test_image_to_outbox(outbox: Outbox):
|
|
82
|
+
img = Image.new('RGB', (60, 30), color=(73, 109, 137))
|
|
83
|
+
img.save('/tmp/image.jpg')
|
|
84
|
+
with open('/tmp/image.jpg', 'rb') as f:
|
|
85
|
+
data = f.read()
|
|
86
|
+
outbox.save(data)
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/tests/test_executor.py
RENAMED
|
@@ -44,6 +44,7 @@ async def test_executor_lifecycle():
|
|
|
44
44
|
|
|
45
45
|
assert not executor.is_running()
|
|
46
46
|
sleep(1)
|
|
47
|
+
# NOTE: It happend that this process became a zombie process which leads to repeated test failures -> restart machine required
|
|
47
48
|
assert_process_is_running('some_executable.sh', False)
|
|
48
49
|
|
|
49
50
|
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/executor.py
RENAMED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import shlex
|
|
5
5
|
from io import BufferedWriter
|
|
6
|
-
from typing import List, Optional
|
|
6
|
+
from typing import List, Optional, Dict
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Executor:
|
|
@@ -16,7 +16,7 @@ class Executor:
|
|
|
16
16
|
|
|
17
17
|
self.path = base_path
|
|
18
18
|
self.log_file_path = f'{self.path}/{log_name}'
|
|
19
|
-
self.log_file:
|
|
19
|
+
self.log_file: Optional[BufferedWriter] = None
|
|
20
20
|
self._process: Optional[asyncio.subprocess.Process] = None # pylint: disable=no-member
|
|
21
21
|
os.makedirs(self.path, exist_ok=True)
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ class Executor:
|
|
|
26
26
|
return self._process
|
|
27
27
|
return None
|
|
28
28
|
|
|
29
|
-
async def start(self, cmd: str, env: Optional[
|
|
29
|
+
async def start(self, cmd: str, env: Optional[Dict[str, str]] = None) -> None:
|
|
30
30
|
"""Start the process with the given command and environment variables."""
|
|
31
31
|
|
|
32
32
|
full_env = os.environ.copy()
|
{learning_loop_node-0.10.5 → learning_loop_node-0.10.7}/learning_loop_node/trainer/tests/conftest.py
RENAMED
|
@@ -29,7 +29,7 @@ async def test_initialized_trainer_node():
|
|
|
29
29
|
trainer._node = node
|
|
30
30
|
trainer._init_new_training(context=Context(organization='zauberzeug', project='demo'),
|
|
31
31
|
details={'categories': [],
|
|
32
|
-
'id': '
|
|
32
|
+
'id': '00000000-0000-0000-0000-000000000012', # version 1.2 of demo project
|
|
33
33
|
'training_number': 0,
|
|
34
34
|
'resolution': 800,
|
|
35
35
|
'flip_rl': False,
|
|
@@ -49,7 +49,7 @@ async def test_initialized_trainer():
|
|
|
49
49
|
trainer._node = node
|
|
50
50
|
trainer._init_new_training(context=Context(organization='zauberzeug', project='demo'),
|
|
51
51
|
details={'categories': [],
|
|
52
|
-
'id': '
|
|
52
|
+
'id': '00000000-0000-0000-0000-000000000012', # version 1.2 of demo project
|
|
53
53
|
'training_number': 0,
|
|
54
54
|
'resolution': 800,
|
|
55
55
|
'flip_rl': False,
|
|
@@ -14,13 +14,13 @@ def trainer_has_error(trainer: TrainerLogic):
|
|
|
14
14
|
return trainer.errors.has_error_for(error_key)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
async def test_successful_detecting(test_initialized_trainer: TestingTrainerLogic):
|
|
17
|
+
async def test_successful_detecting(test_initialized_trainer: TestingTrainerLogic):
|
|
18
18
|
trainer = test_initialized_trainer
|
|
19
19
|
create_active_training_file(trainer, training_state='train_model_uploaded',
|
|
20
|
-
model_uuid_for_detecting='
|
|
21
|
-
|
|
20
|
+
model_uuid_for_detecting='00000000-0000-0000-0000-000000000011') # NOTE: this is the hard coded model uuid for zauberzeug/demo (model version 1.1)
|
|
21
|
+
|
|
22
22
|
_ = asyncio.get_running_loop().create_task(
|
|
23
|
-
trainer._perform_state('
|
|
23
|
+
trainer._perform_state('detecting', TrainerState.Detecting, TrainerState.Detected, trainer._do_detections))
|
|
24
24
|
|
|
25
25
|
await assert_training_state(trainer.training, TrainerState.Detecting, timeout=1, interval=0.001)
|
|
26
26
|
await assert_training_state(trainer.training, TrainerState.Detected, timeout=10, interval=0.001)
|
|
@@ -54,7 +54,7 @@ async def test_abort_upload_model(test_initialized_trainer: TestingTrainerLogic)
|
|
|
54
54
|
async def test_bad_server_response_content(test_initialized_trainer: TestingTrainerLogic):
|
|
55
55
|
"""Set the training state to confusion_matrix_synced and try to upload the model.
|
|
56
56
|
This should fail because the server response is not a valid model id.
|
|
57
|
-
The training should be aborted and the training state should be set to
|
|
57
|
+
The training should be aborted and the training state should be set to ready_for_cleanup."""
|
|
58
58
|
trainer = test_initialized_trainer
|
|
59
59
|
|
|
60
60
|
create_active_training_file(trainer, training_state=TrainerState.ConfusionMatrixSynced)
|
|
@@ -64,10 +64,10 @@ async def test_bad_server_response_content(test_initialized_trainer: TestingTrai
|
|
|
64
64
|
|
|
65
65
|
await assert_training_state(trainer.training, TrainerState.TrainModelUploading, timeout=1, interval=0.001)
|
|
66
66
|
# TODO goes to finished because of the error
|
|
67
|
-
await assert_training_state(trainer.training, TrainerState.
|
|
67
|
+
await assert_training_state(trainer.training, TrainerState.ReadyForCleanup, timeout=2, interval=0.001)
|
|
68
68
|
|
|
69
69
|
assert trainer_has_error(trainer)
|
|
70
|
-
assert trainer.training.training_state == TrainerState.
|
|
70
|
+
assert trainer.training.training_state == TrainerState.ReadyForCleanup
|
|
71
71
|
assert trainer.training.model_uuid_for_detecting is None
|
|
72
72
|
assert trainer.node.last_training_io.load() == trainer.training
|
|
73
73
|
|
|
@@ -81,8 +81,7 @@ async def test_mock_loop_response_example(mocker: MockerFixture, test_initialize
|
|
|
81
81
|
trainer._init_from_last_training()
|
|
82
82
|
|
|
83
83
|
# pylint: disable=protected-access
|
|
84
|
-
|
|
85
|
-
assert result is not None
|
|
84
|
+
await trainer._upload_model_return_new_model_uuid(Context(organization='zauberzeug', project='demo'))
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
def mock_upload_model_for_training(mocker, return_value):
|
|
@@ -59,7 +59,7 @@ class TestingTrainerLogic(TrainerLogic):
|
|
|
59
59
|
await super()._upload_model()
|
|
60
60
|
await asyncio.sleep(0.1) # give tests a bit time to to check for the state
|
|
61
61
|
|
|
62
|
-
async def _upload_model_return_new_model_uuid(self, context: Context) ->
|
|
62
|
+
async def _upload_model_return_new_model_uuid(self, context: Context) -> str:
|
|
63
63
|
await asyncio.sleep(0.1) # give tests a bit time to to check for the state
|
|
64
64
|
result = await super()._upload_model_return_new_model_uuid(context)
|
|
65
65
|
await asyncio.sleep(0.1) # give tests a bit time to to check for the state
|