learning-loop-node 0.10.7__py3-none-any.whl → 0.10.9__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/detector/detector_node.py +1 -1
- learning_loop_node/detector/outbox.py +67 -35
- learning_loop_node/detector/rest/outbox_mode.py +1 -1
- learning_loop_node/tests/annotator/conftest.py +50 -0
- learning_loop_node/{annotation/tests → tests/annotator}/test_annotator_node.py +9 -11
- learning_loop_node/{detector/tests → tests/detector}/conftest.py +30 -4
- learning_loop_node/{detector/inbox_filter/tests → tests/detector/inbox_filter}/test_observation.py +1 -1
- learning_loop_node/{detector/inbox_filter/tests → tests/detector/inbox_filter}/test_relevance_group.py +2 -7
- learning_loop_node/{detector/inbox_filter/tests → tests/detector/inbox_filter}/test_unexpected_observations_count.py +3 -6
- learning_loop_node/tests/detector/pytest.ini +10 -0
- learning_loop_node/{detector/tests → tests/detector}/test_client_communication.py +12 -9
- learning_loop_node/tests/detector/test_outbox.py +96 -0
- learning_loop_node/{detector/tests → tests/detector}/test_relevance_filter.py +8 -6
- learning_loop_node/{detector/tests → tests/detector}/testing_detector.py +3 -3
- learning_loop_node/tests/general/__init__.py +0 -0
- learning_loop_node/tests/general/conftest.py +62 -0
- learning_loop_node/tests/general/pytest.ini +10 -0
- learning_loop_node/tests/{test_data_classes.py → general/test_data_classes.py} +3 -3
- learning_loop_node/tests/{test_downloader.py → general/test_downloader.py} +24 -12
- learning_loop_node/tests/general/test_learning_loop_node.py +20 -0
- learning_loop_node/tests/test_helper.py +20 -9
- learning_loop_node/tests/trainer/__init__.py +0 -0
- learning_loop_node/{trainer/tests → tests/trainer}/conftest.py +32 -3
- learning_loop_node/tests/trainer/pytest.ini +10 -0
- learning_loop_node/{trainer/tests → tests/trainer}/state_helper.py +2 -1
- learning_loop_node/tests/trainer/states/__init__.py +0 -0
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_cleanup.py +2 -2
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_detecting.py +5 -5
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_download_train_model.py +3 -3
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_prepare.py +4 -4
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_sync_confusion_matrix.py +3 -4
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_train.py +4 -4
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_upload_detections.py +6 -6
- learning_loop_node/{trainer/tests → tests/trainer}/states/test_state_upload_model.py +4 -4
- learning_loop_node/{trainer/tests → tests/trainer}/test_errors.py +3 -3
- learning_loop_node/{trainer/tests → tests/trainer}/test_trainer_states.py +4 -4
- learning_loop_node/{trainer/tests → tests/trainer}/testing_trainer_logic.py +2 -2
- learning_loop_node/{tests → trainer}/test_executor.py +1 -1
- learning_loop_node/trainer/trainer_node.py +5 -7
- {learning_loop_node-0.10.7.dist-info → learning_loop_node-0.10.9.dist-info}/METADATA +1 -1
- learning_loop_node-0.10.9.dist-info/RECORD +93 -0
- learning_loop_node/conftest.py +0 -89
- learning_loop_node/detector/tests/test_outbox.py +0 -86
- learning_loop_node/tests/conftest.py +0 -21
- learning_loop_node/tests/test_learning_loop_node.py +0 -18
- learning_loop_node-0.10.7.dist-info/RECORD +0 -87
- /learning_loop_node/{detector/tests → tests/annotator}/__init__.py +0 -0
- /learning_loop_node/{pytest.ini → tests/annotator/pytest.ini} +0 -0
- /learning_loop_node/{trainer/tests → tests/detector}/__init__.py +0 -0
- /learning_loop_node/{trainer/tests/states → tests/detector/inbox_filter}/__init__.py +0 -0
- /learning_loop_node/{detector/tests → tests/detector}/test.jpg +0 -0
- /learning_loop_node/tests/{test_data → general/test_data}/file_1.txt +0 -0
- /learning_loop_node/tests/{test_data → general/test_data}/file_2.txt +0 -0
- /learning_loop_node/tests/{test_data → general/test_data}/model.json +0 -0
- {learning_loop_node-0.10.7.dist-info → learning_loop_node-0.10.9.dist-info}/WHEEL +0 -0
|
@@ -99,7 +99,7 @@ class DetectorNode(Node):
|
|
|
99
99
|
|
|
100
100
|
async def on_shutdown(self) -> None:
|
|
101
101
|
try:
|
|
102
|
-
self.outbox.ensure_continuous_upload_stopped()
|
|
102
|
+
await self.outbox.ensure_continuous_upload_stopped()
|
|
103
103
|
for sid in self.connected_clients:
|
|
104
104
|
# pylint: disable=no-member
|
|
105
105
|
await self.sio.disconnect(sid) # type:ignore
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
1
3
|
import json
|
|
2
4
|
import logging
|
|
3
5
|
import os
|
|
4
6
|
import shutil
|
|
5
|
-
import
|
|
7
|
+
from asyncio import Task
|
|
6
8
|
from dataclasses import asdict
|
|
7
9
|
from datetime import datetime
|
|
8
10
|
from enum import Enum
|
|
@@ -10,9 +12,10 @@ from glob import glob
|
|
|
10
12
|
from io import BufferedReader, TextIOWrapper
|
|
11
13
|
from multiprocessing import Event
|
|
12
14
|
from multiprocessing.synchronize import Event as SyncEvent
|
|
13
|
-
from
|
|
14
|
-
from typing import List, Optional
|
|
15
|
+
from typing import List, Optional, Tuple, Union
|
|
15
16
|
|
|
17
|
+
import PIL
|
|
18
|
+
import PIL.Image # type: ignore
|
|
16
19
|
import requests
|
|
17
20
|
from fastapi.encoders import jsonable_encoder
|
|
18
21
|
|
|
@@ -29,6 +32,7 @@ class OutboxMode(Enum):
|
|
|
29
32
|
class Outbox():
|
|
30
33
|
def __init__(self) -> None:
|
|
31
34
|
self.log = logging.getLogger()
|
|
35
|
+
self.log.setLevel(logging.DEBUG)
|
|
32
36
|
self.path = f'{GLOBALS.data_folder}/outbox'
|
|
33
37
|
os.makedirs(self.path, exist_ok=True)
|
|
34
38
|
|
|
@@ -47,9 +51,15 @@ class Outbox():
|
|
|
47
51
|
self.UPLOAD_TIMEOUT_S = 30
|
|
48
52
|
|
|
49
53
|
self.shutdown_event: SyncEvent = Event()
|
|
50
|
-
self.
|
|
54
|
+
self.upload_task: Optional[Task] = None
|
|
55
|
+
|
|
56
|
+
self.upload_counter = 0
|
|
51
57
|
|
|
52
58
|
def save(self, image: bytes, detections: Optional[Detections] = None, tags: Optional[List[str]] = None) -> None:
|
|
59
|
+
if not self._is_valid_jpg(image):
|
|
60
|
+
self.log.error('Invalid jpg image')
|
|
61
|
+
return
|
|
62
|
+
|
|
53
63
|
if detections is None:
|
|
54
64
|
detections = Detections()
|
|
55
65
|
if not tags:
|
|
@@ -81,42 +91,57 @@ class Outbox():
|
|
|
81
91
|
return
|
|
82
92
|
|
|
83
93
|
self.shutdown_event.clear()
|
|
84
|
-
self.
|
|
85
|
-
self.upload_process.start()
|
|
94
|
+
self.upload_task = asyncio.create_task(self._continuous_upload())
|
|
86
95
|
|
|
87
|
-
def _continuous_upload(self):
|
|
96
|
+
async def _continuous_upload(self):
|
|
88
97
|
self.log.info('continuous upload started')
|
|
89
98
|
assert self.shutdown_event is not None
|
|
90
99
|
while not self.shutdown_event.is_set():
|
|
91
100
|
self.upload()
|
|
92
|
-
|
|
101
|
+
await asyncio.sleep(5)
|
|
93
102
|
self.log.info('continuous upload ended')
|
|
94
103
|
|
|
95
104
|
def upload(self):
|
|
96
105
|
items = self.get_data_files()
|
|
97
|
-
if items:
|
|
98
|
-
self.log.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
if not items:
|
|
107
|
+
self.log.debug('No images found to upload')
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self.log.info('Found %s images to upload', len(items))
|
|
111
|
+
for i in range(0, len(items), self.BATCH_SIZE):
|
|
112
|
+
batch_items = items[i:i+self.BATCH_SIZE]
|
|
113
|
+
if self.shutdown_event.is_set():
|
|
114
|
+
break
|
|
115
|
+
try:
|
|
116
|
+
self._upload_batch(batch_items)
|
|
117
|
+
except Exception:
|
|
118
|
+
self.log.exception('Could not upload files')
|
|
109
119
|
|
|
110
120
|
def _upload_batch(self, items: List[str]):
|
|
111
|
-
|
|
121
|
+
|
|
122
|
+
# NOTE: keys are not relevant for the server, but using a fixed key like 'files'
|
|
123
|
+
# results in a post failure on the first run of the test in a docker environment (WTF)
|
|
124
|
+
|
|
125
|
+
data: List[Tuple[str, Union[TextIOWrapper, BufferedReader]]] = []
|
|
112
126
|
data = [('files', open(f'{item}/image.json', 'r')) for item in items]
|
|
113
127
|
data += [('files', open(f'{item}/image.jpg', 'rb')) for item in items]
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
try:
|
|
130
|
+
response = requests.post(self.target_uri, files=data, timeout=self.UPLOAD_TIMEOUT_S)
|
|
131
|
+
except Exception:
|
|
132
|
+
self.log.exception('Could not upload images')
|
|
133
|
+
return
|
|
134
|
+
finally:
|
|
135
|
+
self.log.info('Closing files')
|
|
136
|
+
for _, file in data:
|
|
137
|
+
file.close()
|
|
138
|
+
|
|
116
139
|
if response.status_code == 200:
|
|
140
|
+
self.upload_counter += len(items)
|
|
117
141
|
for item in items:
|
|
118
142
|
shutil.rmtree(item, ignore_errors=True)
|
|
119
143
|
self.log.info('Uploaded %s images successfully', len(items))
|
|
144
|
+
|
|
120
145
|
elif response.status_code == 422:
|
|
121
146
|
if len(items) == 1:
|
|
122
147
|
self.log.error('Broken content in image: %s\n Skipping.', items[0])
|
|
@@ -129,36 +154,43 @@ class Outbox():
|
|
|
129
154
|
else:
|
|
130
155
|
self.log.error('Could not upload images: %s', response.content)
|
|
131
156
|
|
|
132
|
-
def
|
|
157
|
+
def _is_valid_jpg(self, image: bytes) -> bool:
|
|
158
|
+
try:
|
|
159
|
+
_ = PIL.Image.open(io.BytesIO(image), formats=['JPEG'])
|
|
160
|
+
return True
|
|
161
|
+
except Exception:
|
|
162
|
+
self.log.exception('Invalid jpg image')
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
async def ensure_continuous_upload_stopped(self) -> bool:
|
|
133
166
|
self.log.debug('Outbox: Ensuring continuous upload')
|
|
134
167
|
if not self._upload_process_alive():
|
|
135
168
|
self.log.debug('Upload thread already stopped')
|
|
136
169
|
return True
|
|
137
|
-
|
|
138
|
-
if not
|
|
170
|
+
|
|
171
|
+
if not self.upload_task:
|
|
139
172
|
return True
|
|
140
173
|
|
|
141
174
|
try:
|
|
142
175
|
assert self.shutdown_event is not None
|
|
143
176
|
self.shutdown_event.set()
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
await asyncio.wait_for(self.upload_task, timeout=self.UPLOAD_TIMEOUT_S + 1)
|
|
178
|
+
except asyncio.TimeoutError:
|
|
179
|
+
self.log.error('Upload task did not terminate in time')
|
|
180
|
+
return False
|
|
146
181
|
except Exception:
|
|
147
|
-
self.log.exception('Error while shutting down upload
|
|
148
|
-
|
|
149
|
-
if proc.is_alive():
|
|
150
|
-
self.log.error('Upload thread did not terminate')
|
|
182
|
+
self.log.exception('Error while shutting down upload task: ')
|
|
151
183
|
return False
|
|
152
184
|
|
|
153
185
|
self.log.info('Upload thread terminated')
|
|
154
186
|
return True
|
|
155
187
|
|
|
156
188
|
def _upload_process_alive(self) -> bool:
|
|
157
|
-
return bool(self.
|
|
189
|
+
return bool(self.upload_task and not self.upload_task.done())
|
|
158
190
|
|
|
159
191
|
def get_mode(self) -> OutboxMode:
|
|
160
192
|
''':return: current mode ('continuous_upload' or 'stopped')'''
|
|
161
|
-
if self.
|
|
193
|
+
if self._upload_process_alive():
|
|
162
194
|
current_mode = OutboxMode.CONTINUOUS_UPLOAD
|
|
163
195
|
else:
|
|
164
196
|
current_mode = OutboxMode.STOPPED
|
|
@@ -166,7 +198,7 @@ class Outbox():
|
|
|
166
198
|
self.log.debug('Outbox: Current mode is %s', current_mode)
|
|
167
199
|
return current_mode
|
|
168
200
|
|
|
169
|
-
def set_mode(self, mode: OutboxMode
|
|
201
|
+
async def set_mode(self, mode: Union[OutboxMode, str]) -> None:
|
|
170
202
|
''':param mode: 'continuous_upload' or 'stopped'
|
|
171
203
|
:raises ValueError: if mode is not a valid OutboxMode
|
|
172
204
|
:raises TimeoutError: if the upload thread does not terminate within 31 seconds with mode='stopped'
|
|
@@ -178,7 +210,7 @@ class Outbox():
|
|
|
178
210
|
self.ensure_continuous_upload()
|
|
179
211
|
elif mode == OutboxMode.STOPPED:
|
|
180
212
|
try:
|
|
181
|
-
self.ensure_continuous_upload_stopped()
|
|
213
|
+
await self.ensure_continuous_upload_stopped()
|
|
182
214
|
except TimeoutError as e:
|
|
183
215
|
raise TimeoutError(f'Upload thread did not terminate within {self.UPLOAD_TIMEOUT_S} seconds.') from e
|
|
184
216
|
|
|
@@ -26,7 +26,7 @@ async def put_outbox_mode(request: Request):
|
|
|
26
26
|
outbox: Outbox = request.app.outbox
|
|
27
27
|
content = str(await request.body(), 'utf-8')
|
|
28
28
|
try:
|
|
29
|
-
outbox.set_mode(content)
|
|
29
|
+
await outbox.set_mode(content)
|
|
30
30
|
except TimeoutError as e:
|
|
31
31
|
raise HTTPException(202, 'Setting has not completed, yet: ' + str(e)) from e
|
|
32
32
|
except ValueError as e:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from ...globals import GLOBALS
|
|
9
|
+
from ...loop_communication import LoopCommunicator
|
|
10
|
+
|
|
11
|
+
# ====================================== REDUNDANT FIXTURES IN ALL CONFTESTS ! ======================================
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture()
|
|
15
|
+
async def setup_test_project(): # pylint: disable=redefined-outer-name
|
|
16
|
+
loop_communicator = LoopCommunicator()
|
|
17
|
+
await loop_communicator.delete("/zauberzeug/projects/pytest_nodelib_annotator?keep_images=true")
|
|
18
|
+
await asyncio.sleep(1)
|
|
19
|
+
project_conf = {
|
|
20
|
+
'project_name': 'pytest_nodelib_annotator', 'inbox': 0, 'annotate': 0, 'review': 0, 'complete': 3, 'image_style': 'beautiful',
|
|
21
|
+
'box_categories': 2, 'point_categories': 2, 'segmentation_categories': 2, 'thumbs': False, 'tags': 0,
|
|
22
|
+
'trainings': 1, 'box_detections': 3, 'box_annotations': 0}
|
|
23
|
+
assert (await loop_communicator.post("/zauberzeug/projects/generator", json=project_conf)).status_code == 200
|
|
24
|
+
yield
|
|
25
|
+
await loop_communicator.delete("/zauberzeug/projects/pytest_nodelib_annotator?keep_images=true")
|
|
26
|
+
await loop_communicator.shutdown()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(autouse=True, scope='session')
|
|
30
|
+
def clear_loggers():
|
|
31
|
+
"""Remove handlers from all loggers"""
|
|
32
|
+
# see https://github.com/pytest-dev/pytest/issues/5502
|
|
33
|
+
yield
|
|
34
|
+
|
|
35
|
+
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
|
|
36
|
+
for logger in loggers:
|
|
37
|
+
if not isinstance(logger, logging.Logger):
|
|
38
|
+
continue
|
|
39
|
+
handlers = getattr(logger, 'handlers', [])
|
|
40
|
+
for handler in handlers:
|
|
41
|
+
logger.removeHandler(handler)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(autouse=True, scope='function')
|
|
45
|
+
def data_folder():
|
|
46
|
+
GLOBALS.data_folder = '/tmp/learning_loop_lib_data'
|
|
47
|
+
shutil.rmtree(GLOBALS.data_folder, ignore_errors=True)
|
|
48
|
+
os.makedirs(GLOBALS.data_folder, exist_ok=True)
|
|
49
|
+
yield
|
|
50
|
+
shutil.rmtree(GLOBALS.data_folder, ignore_errors=True)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import os
|
|
3
2
|
from dataclasses import asdict
|
|
4
3
|
from typing import Dict
|
|
@@ -6,10 +5,10 @@ from typing import Dict
|
|
|
6
5
|
import pytest
|
|
7
6
|
from fastapi.encoders import jsonable_encoder
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
|
|
8
|
+
from ...annotation.annotator_logic import AnnotatorLogic
|
|
9
|
+
from ...annotation.annotator_node import AnnotatorNode
|
|
10
|
+
from ...data_classes import (AnnotationData, AnnotationEventType, Category, CategoryType, Context, Point, ToolOutput,
|
|
11
|
+
UserInput)
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class MockedAnnotatatorLogic(AnnotatorLogic):
|
|
@@ -27,8 +26,8 @@ def default_user_input() -> UserInput:
|
|
|
27
26
|
annotation_data = AnnotationData(
|
|
28
27
|
coordinate=Point(x=0, y=0),
|
|
29
28
|
event_type=AnnotationEventType.LeftMouseDown,
|
|
30
|
-
context=Context(organization='zauberzeug', project='
|
|
31
|
-
image_uuid='
|
|
29
|
+
context=Context(organization='zauberzeug', project='pytest_nodelib_annotator'),
|
|
30
|
+
image_uuid='10f7d7d2-4076-7006-af37-fa28a6a848ae',
|
|
32
31
|
category=Category(id='some_id', name='category_1', description='',
|
|
33
32
|
hotkey='', color='', type=CategoryType.Segmentation)
|
|
34
33
|
)
|
|
@@ -39,13 +38,12 @@ def default_user_input() -> UserInput:
|
|
|
39
38
|
|
|
40
39
|
@pytest.mark.asyncio
|
|
41
40
|
async def test_image_download(setup_test_project): # pylint: disable=unused-argument
|
|
42
|
-
|
|
43
|
-
image_path = '/tmp/learning_loop_lib_data/zauberzeug/pytest_p/images/f786350c-89ca-9424-9b00-720a9a85fe09.jpg'
|
|
41
|
+
image_folder = '/tmp/learning_loop_lib_data/zauberzeug/pytest_nodelib_annotator/images'
|
|
44
42
|
|
|
45
|
-
assert os.path.exists(
|
|
43
|
+
assert os.path.exists(image_folder) is False or len(os.listdir(image_folder)) == 0
|
|
46
44
|
|
|
47
45
|
node = AnnotatorNode(name="", uuid="", annotator_logic=MockedAnnotatatorLogic())
|
|
48
46
|
user_input = default_user_input()
|
|
49
47
|
_ = await node._handle_user_input(jsonable_encoder(asdict(user_input))) # pylint: disable=protected-access
|
|
50
48
|
|
|
51
|
-
assert os.path.exists(
|
|
49
|
+
assert os.path.exists(image_folder) is True and len(os.listdir(image_folder)) == 1
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import shutil
|
|
1
2
|
import asyncio
|
|
2
3
|
import logging
|
|
3
4
|
import multiprocessing
|
|
@@ -11,10 +12,9 @@ import pytest
|
|
|
11
12
|
import socketio
|
|
12
13
|
import uvicorn
|
|
13
14
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
|
|
15
|
+
from ...detector.detector_node import DetectorNode
|
|
16
|
+
from ...detector.outbox import Outbox
|
|
17
|
+
from ...globals import GLOBALS
|
|
18
18
|
from .testing_detector import TestingDetectorLogic
|
|
19
19
|
|
|
20
20
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -112,3 +112,29 @@ async def sio_client() -> AsyncGenerator[socketio.AsyncClient, None]:
|
|
|
112
112
|
def get_outbox_files(outbox: Outbox):
|
|
113
113
|
files = glob(f'{outbox.path}/**/*', recursive=True)
|
|
114
114
|
return [file for file in files if os.path.isfile(file)]
|
|
115
|
+
|
|
116
|
+
# ====================================== REDUNDANT FIXTURES IN ALL CONFTESTS ! ======================================
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.fixture(autouse=True, scope='session')
|
|
120
|
+
def clear_loggers():
|
|
121
|
+
"""Remove handlers from all loggers"""
|
|
122
|
+
# see https://github.com/pytest-dev/pytest/issues/5502
|
|
123
|
+
yield
|
|
124
|
+
|
|
125
|
+
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
|
|
126
|
+
for logger in loggers:
|
|
127
|
+
if not isinstance(logger, logging.Logger):
|
|
128
|
+
continue
|
|
129
|
+
handlers = getattr(logger, 'handlers', [])
|
|
130
|
+
for handler in handlers:
|
|
131
|
+
logger.removeHandler(handler)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.fixture(autouse=True, scope='function')
|
|
135
|
+
def data_folder():
|
|
136
|
+
GLOBALS.data_folder = '/tmp/learning_loop_lib_data'
|
|
137
|
+
shutil.rmtree(GLOBALS.data_folder, ignore_errors=True)
|
|
138
|
+
os.makedirs(GLOBALS.data_folder, exist_ok=True)
|
|
139
|
+
yield
|
|
140
|
+
shutil.rmtree(GLOBALS.data_folder, ignore_errors=True)
|
|
@@ -5,13 +5,8 @@ from typing import List
|
|
|
5
5
|
|
|
6
6
|
from dacite import from_dict
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
PointDetection,
|
|
11
|
-
SegmentationDetection,
|
|
12
|
-
Shape)
|
|
13
|
-
from learning_loop_node.detector.inbox_filter.cam_observation_history import \
|
|
14
|
-
CamObservationHistory
|
|
8
|
+
from ....data_classes.detections import BoxDetection, Detections, Point, PointDetection, SegmentationDetection, Shape
|
|
9
|
+
from ....detector.inbox_filter.cam_observation_history import CamObservationHistory
|
|
15
10
|
|
|
16
11
|
dirt_detection = BoxDetection(category_name='dirt', x=0, y=0, width=100, height=100,
|
|
17
12
|
category_id='xyz', model_name='test_model', confidence=.3)
|
|
@@ -3,12 +3,9 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from learning_loop_node.detector.inbox_filter.relevance_filter import \
|
|
10
|
-
RelevanceFilter
|
|
11
|
-
from learning_loop_node.detector.outbox import Outbox
|
|
6
|
+
from ....data_classes.detections import BoxDetection, Detections, PointDetection
|
|
7
|
+
from ....detector.inbox_filter.relevance_filter import RelevanceFilter
|
|
8
|
+
from ....detector.outbox import Outbox
|
|
12
9
|
|
|
13
10
|
h_conf_box_det = BoxDetection(category_name='dirt', x=0, y=0, width=100,
|
|
14
11
|
height=100, category_id='xyz', confidence=.9, model_name='test_model',)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[pytest]
|
|
2
|
+
python_files = test_*.py
|
|
3
|
+
asyncio_mode = auto
|
|
4
|
+
|
|
5
|
+
cache_dir = /tmp/pytest_cache
|
|
6
|
+
|
|
7
|
+
# for debbuging tests:
|
|
8
|
+
; log_cli_level = INFO
|
|
9
|
+
; log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
|
|
10
|
+
; log_cli_date_format=%Y-%m-%d %H:%M:%S
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import os
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
import requests
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
8
|
+
from ...data_classes import ModelInformation
|
|
9
|
+
from ...detector.detector_node import DetectorNode
|
|
10
|
+
from ...globals import GLOBALS
|
|
11
|
+
from .conftest import get_outbox_files
|
|
12
12
|
from .testing_detector import TestingDetectorLogic
|
|
13
13
|
|
|
14
|
+
file_path = os.path.abspath(__file__)
|
|
15
|
+
test_image_path = os.path.join(os.path.dirname(file_path), 'test.jpg')
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
@pytest.mark.asyncio
|
|
16
19
|
async def test_detector_path(test_detector_node: DetectorNode):
|
|
@@ -20,7 +23,7 @@ async def test_detector_path(test_detector_node: DetectorNode):
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
async def test_sio_detect(test_detector_node, sio_client):
|
|
23
|
-
with open(
|
|
26
|
+
with open(test_image_path, 'rb') as f:
|
|
24
27
|
image_bytes = f.read()
|
|
25
28
|
|
|
26
29
|
await asyncio.sleep(5)
|
|
@@ -44,7 +47,7 @@ async def test_sio_detect(test_detector_node, sio_client):
|
|
|
44
47
|
|
|
45
48
|
@pytest.mark.parametrize('grouping_key', ['mac', 'camera_id'])
|
|
46
49
|
def test_rest_detect(test_detector_node: DetectorNode, grouping_key: str):
|
|
47
|
-
image = {('file', open(
|
|
50
|
+
image = {('file', open(test_image_path, 'rb'))}
|
|
48
51
|
headers = {grouping_key: '0:0:0:0', 'tags': 'some_tag'}
|
|
49
52
|
|
|
50
53
|
assert isinstance(test_detector_node.detector_logic, TestingDetectorLogic)
|
|
@@ -71,7 +74,7 @@ def test_rest_detect(test_detector_node: DetectorNode, grouping_key: str):
|
|
|
71
74
|
def test_rest_upload(test_detector_node: DetectorNode):
|
|
72
75
|
assert len(get_outbox_files(test_detector_node.outbox)) == 0
|
|
73
76
|
|
|
74
|
-
image = {('files', open(
|
|
77
|
+
image = {('files', open(test_image_path, 'rb'))}
|
|
75
78
|
response = requests.post(f'http://localhost:{GLOBALS.detector_port}/upload', files=image, timeout=30)
|
|
76
79
|
assert response.status_code == 200
|
|
77
80
|
assert len(get_outbox_files(test_detector_node.outbox)) == 2, 'There should be one image and one .json file.'
|
|
@@ -81,7 +84,7 @@ def test_rest_upload(test_detector_node: DetectorNode):
|
|
|
81
84
|
async def test_sio_upload(test_detector_node: DetectorNode, sio_client):
|
|
82
85
|
assert len(get_outbox_files(test_detector_node.outbox)) == 0
|
|
83
86
|
|
|
84
|
-
with open(
|
|
87
|
+
with open(test_image_path, 'rb') as f:
|
|
85
88
|
image_bytes = f.read()
|
|
86
89
|
result = await sio_client.call('upload', {'image': image_bytes})
|
|
87
90
|
assert result is None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
from ...data_classes import Detections
|
|
10
|
+
from ...detector.detector_node import DetectorNode
|
|
11
|
+
from ...detector.outbox import Outbox
|
|
12
|
+
from ...globals import GLOBALS
|
|
13
|
+
|
|
14
|
+
# pylint: disable=redefined-outer-name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture()
|
|
18
|
+
async def test_outbox():
|
|
19
|
+
os.environ['LOOP_ORGANIZATION'] = 'zauberzeug'
|
|
20
|
+
os.environ['LOOP_PROJECT'] = 'demo'
|
|
21
|
+
shutil.rmtree(f'{GLOBALS.data_folder}/outbox', ignore_errors=True)
|
|
22
|
+
test_outbox = Outbox()
|
|
23
|
+
|
|
24
|
+
yield test_outbox
|
|
25
|
+
await test_outbox.set_mode('stopped')
|
|
26
|
+
shutil.rmtree(test_outbox.path, ignore_errors=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
async def test_files_are_automatically_uploaded_by_node(test_detector_node: DetectorNode):
|
|
31
|
+
test_detector_node.outbox.save(get_test_image_binary(), Detections())
|
|
32
|
+
assert await wait_for_outbox_count(test_detector_node.outbox, 1)
|
|
33
|
+
assert await wait_for_outbox_count(test_detector_node.outbox, 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_set_outbox_mode(test_outbox: Outbox):
|
|
38
|
+
await test_outbox.set_mode('stopped')
|
|
39
|
+
test_outbox.save(get_test_image_binary())
|
|
40
|
+
assert await wait_for_outbox_count(test_outbox, 1)
|
|
41
|
+
await asyncio.sleep(6)
|
|
42
|
+
assert await wait_for_outbox_count(test_outbox, 1), 'File was cleared even though outbox should be stopped'
|
|
43
|
+
|
|
44
|
+
await test_outbox.set_mode('continuous_upload')
|
|
45
|
+
assert await wait_for_outbox_count(test_outbox, 0), 'File was not cleared even though outbox should be in continuous_upload'
|
|
46
|
+
assert test_outbox.upload_counter == 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_outbox_upload_is_successful(test_outbox: Outbox):
|
|
51
|
+
test_outbox.save(get_test_image_binary())
|
|
52
|
+
await asyncio.sleep(1)
|
|
53
|
+
test_outbox.save(get_test_image_binary())
|
|
54
|
+
assert await wait_for_outbox_count(test_outbox, 2)
|
|
55
|
+
test_outbox.upload()
|
|
56
|
+
assert await wait_for_outbox_count(test_outbox, 0)
|
|
57
|
+
assert test_outbox.upload_counter == 2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_invalid_jpg_is_not_saved(test_outbox: Outbox):
|
|
62
|
+
invalid_bytes = b'invalid jpg'
|
|
63
|
+
test_outbox.save(invalid_bytes)
|
|
64
|
+
assert len(test_outbox.get_data_files()) == 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ------------------------------ Helper functions --------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_test_image_binary():
|
|
71
|
+
img = Image.new('RGB', (60, 30), color=(73, 109, 137))
|
|
72
|
+
# convert img to jpg binary
|
|
73
|
+
|
|
74
|
+
img_byte_arr = io.BytesIO()
|
|
75
|
+
img.save(img_byte_arr, format='JPEG')
|
|
76
|
+
img_byte_arr = img_byte_arr.getvalue()
|
|
77
|
+
return img_byte_arr
|
|
78
|
+
|
|
79
|
+
# return img.tobytes() # NOT WORKING
|
|
80
|
+
|
|
81
|
+
# img.save('/tmp/image.jpg')
|
|
82
|
+
# with open('/tmp/image.jpg', 'rb') as f:
|
|
83
|
+
# data = f.read()
|
|
84
|
+
|
|
85
|
+
# return data
|
|
86
|
+
|
|
87
|
+
# img = np.ones((300, 300, 3), np.uint8)*255 # NOT WORKING
|
|
88
|
+
# return img.tobytes()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def wait_for_outbox_count(outbox: Outbox, count: int, timeout: int = 10) -> bool:
|
|
92
|
+
for _ in range(timeout):
|
|
93
|
+
if len(outbox.get_data_files()) == count:
|
|
94
|
+
return True
|
|
95
|
+
await asyncio.sleep(1)
|
|
96
|
+
return False
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import os
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
PointDetection)
|
|
9
|
-
|
|
7
|
+
from ...data_classes import BoxDetection, Detections, PointDetection
|
|
8
|
+
from ...detector.detector_node import DetectorNode
|
|
10
9
|
from .conftest import get_outbox_files
|
|
11
10
|
from .testing_detector import TestingDetectorLogic
|
|
12
11
|
|
|
12
|
+
file_path = os.path.abspath(__file__)
|
|
13
|
+
test_image_path = os.path.join(os.path.dirname(file_path), 'test.jpg')
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@pytest.mark.parametrize('autoupload, expected_file_count', [(None, 2), ('all', 4)])
|
|
15
17
|
async def test_filter_is_used_by_node(test_detector_node: DetectorNode, autoupload, expected_file_count):
|
|
@@ -28,11 +30,11 @@ async def test_filter_is_used_by_node(test_detector_node: DetectorNode, autouplo
|
|
|
28
30
|
assert test_detector_node.outbox.path.startswith('/tmp')
|
|
29
31
|
assert len(get_outbox_files(test_detector_node.outbox)) == 0
|
|
30
32
|
|
|
31
|
-
image = np.fromfile(
|
|
33
|
+
image = np.fromfile(file=test_image_path, dtype=np.uint8)
|
|
32
34
|
_ = await test_detector_node.get_detections(image, '00:.....', tags=[], autoupload=autoupload)
|
|
33
35
|
# NOTE adding second images with identical detections
|
|
34
36
|
_ = await test_detector_node.get_detections(image, '00:.....', tags=[], autoupload=autoupload)
|
|
35
37
|
await asyncio.sleep(.5) # files are stored asynchronously
|
|
36
38
|
|
|
37
|
-
assert len(get_outbox_files(test_detector_node.outbox)) == expected_file_count
|
|
39
|
+
assert len(get_outbox_files(test_detector_node.outbox)) == expected_file_count, \
|
|
38
40
|
'There should be 1 image and 1 .json file for every detection in the outbox'
|
|
@@ -2,9 +2,9 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
5
|
+
from ...data_classes import Detections
|
|
6
|
+
from ...detector.detector_logic import DetectorLogic
|
|
7
|
+
from ..test_helper import get_dummy_detections
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TestingDetectorLogic(DetectorLogic):
|
|
File without changes
|