learning-loop-node 0.14.0__py3-none-any.whl → 0.16.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/annotation/annotator_node.py +7 -1
- learning_loop_node/data_classes/__init__.py +34 -8
- learning_loop_node/data_classes/general.py +10 -11
- learning_loop_node/data_classes/image_metadata.py +5 -0
- learning_loop_node/data_classes/training.py +3 -2
- learning_loop_node/data_exchanger.py +3 -3
- learning_loop_node/detector/detector_logic.py +6 -1
- learning_loop_node/detector/detector_node.py +197 -139
- learning_loop_node/detector/outbox.py +2 -2
- learning_loop_node/node.py +49 -36
- learning_loop_node/rest.py +3 -2
- learning_loop_node/tests/annotator/test_annotator_node.py +4 -1
- learning_loop_node/tests/detector/conftest.py +9 -0
- learning_loop_node/tests/detector/test_outbox.py +27 -15
- learning_loop_node/tests/detector/test_relevance_filter.py +3 -3
- learning_loop_node/tests/trainer/conftest.py +2 -2
- learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +4 -1
- learning_loop_node/trainer/trainer_logic_generic.py +19 -4
- learning_loop_node/trainer/trainer_node.py +4 -3
- {learning_loop_node-0.14.0.dist-info → learning_loop_node-0.16.0.dist-info}/METADATA +3 -1
- {learning_loop_node-0.14.0.dist-info → learning_loop_node-0.16.0.dist-info}/RECORD +22 -22
- {learning_loop_node-0.14.0.dist-info → learning_loop_node-0.16.0.dist-info}/WHEEL +0 -0
learning_loop_node/node.py
CHANGED
|
@@ -4,9 +4,9 @@ from .helpers import log_conf # pylint: disable=unused-import
|
|
|
4
4
|
|
|
5
5
|
# isort: split
|
|
6
6
|
# pylint: disable=wrong-import-order,ungrouped-imports
|
|
7
|
-
|
|
8
7
|
import asyncio
|
|
9
8
|
import logging
|
|
9
|
+
import os
|
|
10
10
|
import ssl
|
|
11
11
|
import sys
|
|
12
12
|
from abc import abstractmethod
|
|
@@ -32,7 +32,12 @@ class NodeConnectionError(Exception):
|
|
|
32
32
|
|
|
33
33
|
class Node(FastAPI):
|
|
34
34
|
|
|
35
|
-
def __init__(self,
|
|
35
|
+
def __init__(self,
|
|
36
|
+
name: str, *,
|
|
37
|
+
uuid: Optional[str] = None,
|
|
38
|
+
node_type: str = 'node',
|
|
39
|
+
needs_login: bool = True,
|
|
40
|
+
needs_sio: bool = True) -> None:
|
|
36
41
|
"""Base class for all nodes. A node is a process that communicates with the zauberzeug learning loop.
|
|
37
42
|
This class provides the basic functionality to connect to the learning loop via socket.io and to exchange data.
|
|
38
43
|
|
|
@@ -42,6 +47,7 @@ class Node(FastAPI):
|
|
|
42
47
|
and stored in f'{GLOBALS.data_folder}/uuids.json'.
|
|
43
48
|
From the second run, the uuid is recovered based on the name of the node.
|
|
44
49
|
needs_login (bool): If True, the node will try to login to the learning loop.
|
|
50
|
+
needs_sio (bool): If True, the node will try to establish and keep a socket.io connection to the loop.
|
|
45
51
|
"""
|
|
46
52
|
|
|
47
53
|
super().__init__(lifespan=self.lifespan)
|
|
@@ -49,13 +55,16 @@ class Node(FastAPI):
|
|
|
49
55
|
self.name = name
|
|
50
56
|
self.uuid = uuid or read_or_create_uuid(self.name)
|
|
51
57
|
self.needs_login = needs_login
|
|
58
|
+
self._needs_sio = needs_sio
|
|
59
|
+
if needs_sio and not needs_login:
|
|
60
|
+
raise ValueError('A node that needs sio must also need login')
|
|
52
61
|
|
|
53
62
|
self.log = logging.getLogger('Node')
|
|
54
63
|
self.init_loop_communicator()
|
|
55
64
|
self.data_exchanger = DataExchanger(None, self.loop_communicator)
|
|
56
65
|
|
|
57
66
|
self.startup_datetime = datetime.now()
|
|
58
|
-
self.
|
|
67
|
+
self.sio_client: Optional[AsyncClient] = None
|
|
59
68
|
self.status = NodeStatus(id=self.uuid, name=self.name)
|
|
60
69
|
|
|
61
70
|
self.sio_headers = {'organization': self.loop_communicator.organization,
|
|
@@ -64,7 +73,7 @@ class Node(FastAPI):
|
|
|
64
73
|
|
|
65
74
|
self.repeat_task: Any = None
|
|
66
75
|
self.socket_connection_broken = False
|
|
67
|
-
self._skip_repeat_loop =
|
|
76
|
+
self._skip_repeat_loop = os.environ.get('SKIP_REPEAT_ON_START', '0') in ('True', 'true', '1')
|
|
68
77
|
|
|
69
78
|
self.include_router(router)
|
|
70
79
|
|
|
@@ -78,23 +87,18 @@ class Node(FastAPI):
|
|
|
78
87
|
|
|
79
88
|
self._client_session: Optional[aiohttp.ClientSession] = None
|
|
80
89
|
|
|
81
|
-
def log_status_on_change(self, current_state_str: str, full_status: Any):
|
|
90
|
+
def log_status_on_change(self, current_state_str: str, full_status: Any) -> None:
|
|
82
91
|
if self.previous_state != current_state_str:
|
|
83
92
|
self.previous_state = current_state_str
|
|
84
93
|
self.log.info('Status changed to %s', full_status)
|
|
85
94
|
else:
|
|
86
95
|
self.log.debug('sending status %s', full_status)
|
|
87
96
|
|
|
88
|
-
def init_loop_communicator(self):
|
|
97
|
+
def init_loop_communicator(self) -> None:
|
|
98
|
+
"""Initialize the loop communicator and set the websocket url."""
|
|
89
99
|
self.loop_communicator = LoopCommunicator()
|
|
90
100
|
self.websocket_url = self.loop_communicator.websocket_url()
|
|
91
101
|
|
|
92
|
-
@property
|
|
93
|
-
def sio_client(self) -> AsyncClient:
|
|
94
|
-
if self._sio_client is None:
|
|
95
|
-
raise Exception('sio_client not yet initialized')
|
|
96
|
-
return self._sio_client
|
|
97
|
-
|
|
98
102
|
# --------------------------------------------------- APPLICATION LIFECYCLE ---------------------------------------------------
|
|
99
103
|
@asynccontextmanager
|
|
100
104
|
async def lifespan(self, app: FastAPI): # pylint: disable=unused-argument
|
|
@@ -114,7 +118,7 @@ class Node(FastAPI):
|
|
|
114
118
|
except asyncio.CancelledError:
|
|
115
119
|
pass
|
|
116
120
|
|
|
117
|
-
async def _on_startup(self):
|
|
121
|
+
async def _on_startup(self) -> None:
|
|
118
122
|
self.log.info('received "startup" lifecycle-event - connecting to loop')
|
|
119
123
|
try:
|
|
120
124
|
await self.reconnect_to_loop()
|
|
@@ -124,17 +128,22 @@ class Node(FastAPI):
|
|
|
124
128
|
await self.on_startup()
|
|
125
129
|
self.log.info('successfully finished on_startup')
|
|
126
130
|
|
|
127
|
-
async def _on_shutdown(self):
|
|
131
|
+
async def _on_shutdown(self) -> None:
|
|
128
132
|
self.log.info('received "shutdown" lifecycle-event')
|
|
129
133
|
await self.loop_communicator.shutdown()
|
|
130
|
-
if self.
|
|
131
|
-
await self.
|
|
134
|
+
if self.sio_client is not None:
|
|
135
|
+
await self.sio_client.disconnect()
|
|
132
136
|
if self._client_session is not None:
|
|
133
137
|
await self._client_session.close()
|
|
134
138
|
self.log.info('successfully disconnected from loop.')
|
|
135
139
|
await self.on_shutdown()
|
|
136
140
|
|
|
137
141
|
async def repeat_loop(self) -> None:
|
|
142
|
+
"""Executed every `repeat_loop_cycle_sec` seconds.
|
|
143
|
+
Triggers the abstract method `on_repeat` which should be implemented by the subclass.
|
|
144
|
+
If `needs_sio` is True, it ensures that the socket.io connection is established before calling on_repeat.
|
|
145
|
+
"""
|
|
146
|
+
|
|
138
147
|
while True:
|
|
139
148
|
if self._skip_repeat_loop:
|
|
140
149
|
self.log.debug('node is muted, skipping repeat loop')
|
|
@@ -142,7 +151,8 @@ class Node(FastAPI):
|
|
|
142
151
|
continue
|
|
143
152
|
try:
|
|
144
153
|
async with self.repeat_loop_lock:
|
|
145
|
-
|
|
154
|
+
if self._needs_sio:
|
|
155
|
+
await self._ensure_sio_connection()
|
|
146
156
|
await self.on_repeat()
|
|
147
157
|
except asyncio.CancelledError:
|
|
148
158
|
return
|
|
@@ -153,14 +163,17 @@ class Node(FastAPI):
|
|
|
153
163
|
|
|
154
164
|
await asyncio.sleep(self.repeat_loop_cycle_sec)
|
|
155
165
|
|
|
156
|
-
async def _ensure_sio_connection(self):
|
|
157
|
-
if
|
|
166
|
+
async def _ensure_sio_connection(self) -> None:
|
|
167
|
+
"""Call reconnect_to_loop if the socket.io connection is broken or not established."""
|
|
168
|
+
if self.socket_connection_broken or self.sio_client is None or not self.sio_client.connected:
|
|
158
169
|
self.log.info('Reconnecting to loop via sio due to %s',
|
|
159
170
|
'broken connection' if self.socket_connection_broken else 'no connection')
|
|
160
171
|
await self.reconnect_to_loop()
|
|
161
172
|
|
|
162
|
-
async def reconnect_to_loop(self):
|
|
173
|
+
async def reconnect_to_loop(self) -> None:
|
|
163
174
|
"""Initialize the loop communicator, log in if needed and reconnect to the loop via socket.io."""
|
|
175
|
+
if not self._needs_sio:
|
|
176
|
+
return
|
|
164
177
|
self.init_loop_communicator()
|
|
165
178
|
await self.loop_communicator.backend_ready(timeout=5)
|
|
166
179
|
if self.needs_login:
|
|
@@ -174,13 +187,13 @@ class Node(FastAPI):
|
|
|
174
187
|
|
|
175
188
|
self.socket_connection_broken = False
|
|
176
189
|
|
|
177
|
-
def set_skip_repeat_loop(self, value: bool):
|
|
190
|
+
def set_skip_repeat_loop(self, value: bool) -> None:
|
|
178
191
|
self._skip_repeat_loop = value
|
|
179
192
|
self.log.info('node is muted: %s', value)
|
|
180
193
|
|
|
181
194
|
# --------------------------------------------------- SOCKET.IO ---------------------------------------------------
|
|
182
195
|
|
|
183
|
-
async def _reconnect_socketio(self):
|
|
196
|
+
async def _reconnect_socketio(self) -> None:
|
|
184
197
|
"""Create a socket.io client, connect it to the learning loop and register its events.
|
|
185
198
|
The current client is disconnected and deleted if it already exists."""
|
|
186
199
|
|
|
@@ -188,7 +201,7 @@ class Node(FastAPI):
|
|
|
188
201
|
cookies = self.loop_communicator.get_cookies()
|
|
189
202
|
self.log.debug('HTTP Cookies: %s\n', cookies)
|
|
190
203
|
|
|
191
|
-
if self.
|
|
204
|
+
if self.sio_client is not None:
|
|
192
205
|
try:
|
|
193
206
|
await self.sio_client.disconnect()
|
|
194
207
|
self.log.info('disconnected from loop via sio')
|
|
@@ -199,7 +212,7 @@ class Node(FastAPI):
|
|
|
199
212
|
'Did not receive disconnect event from loop within 5 seconds.\nContinuing with new connection...')
|
|
200
213
|
except Exception as e:
|
|
201
214
|
self.log.warning('Could not disconnect from loop via sio: %s.\nIgnoring...', e)
|
|
202
|
-
self.
|
|
215
|
+
self.sio_client = None
|
|
203
216
|
|
|
204
217
|
connector = None
|
|
205
218
|
if self.loop_communicator.ssl_cert_path:
|
|
@@ -217,55 +230,55 @@ class Node(FastAPI):
|
|
|
217
230
|
else:
|
|
218
231
|
self._client_session = aiohttp.ClientSession(connector=connector)
|
|
219
232
|
|
|
220
|
-
self.
|
|
233
|
+
self.sio_client = AsyncClient(request_timeout=20, http_session=self._client_session)
|
|
221
234
|
|
|
222
235
|
# pylint: disable=protected-access
|
|
223
|
-
self.
|
|
236
|
+
self.sio_client._trigger_event = ensure_socket_response(self.sio_client._trigger_event)
|
|
224
237
|
|
|
225
|
-
@self.
|
|
238
|
+
@self.sio_client.event
|
|
226
239
|
async def connect():
|
|
227
240
|
self.log.info('received "connect" via sio from loop.')
|
|
228
241
|
self.CONNECTED_TO_LOOP.set()
|
|
229
242
|
self.DISCONNECTED_FROM_LOOP.clear()
|
|
230
243
|
|
|
231
|
-
@self.
|
|
244
|
+
@self.sio_client.event
|
|
232
245
|
async def disconnect():
|
|
233
246
|
self.log.info('received "disconnect" via sio from loop.')
|
|
234
247
|
self.DISCONNECTED_FROM_LOOP.set()
|
|
235
248
|
self.CONNECTED_TO_LOOP.clear()
|
|
236
249
|
|
|
237
|
-
@self.
|
|
250
|
+
@self.sio_client.event
|
|
238
251
|
async def restart():
|
|
239
252
|
self.log.info('received "restart" via sio from loop -> restarting node.')
|
|
240
253
|
sys.exit(0)
|
|
241
254
|
|
|
242
|
-
self.register_sio_events(self.
|
|
255
|
+
self.register_sio_events(self.sio_client)
|
|
243
256
|
try:
|
|
244
|
-
await self.
|
|
257
|
+
await self.sio_client.connect(f"{self.websocket_url}", headers=self.sio_headers, socketio_path="/ws/socket.io")
|
|
245
258
|
except Exception as e:
|
|
246
259
|
self.log.exception('Could not connect socketio client to loop')
|
|
247
260
|
raise NodeConnectionError('Could not connect socketio client to loop') from e
|
|
248
261
|
|
|
249
|
-
if not self.
|
|
262
|
+
if not self.sio_client.connected:
|
|
250
263
|
self.log.exception('Could not connect socketio client to loop')
|
|
251
264
|
raise NodeConnectionError('Could not connect socketio client to loop')
|
|
252
265
|
|
|
253
266
|
# --------------------------------------------------- ABSTRACT METHODS ---------------------------------------------------
|
|
254
267
|
|
|
255
268
|
@abstractmethod
|
|
256
|
-
async def on_startup(self):
|
|
269
|
+
async def on_startup(self) -> None:
|
|
257
270
|
"""This method is called when the node is started.
|
|
258
271
|
Note: In this method the sio connection is not yet established!"""
|
|
259
272
|
|
|
260
273
|
@abstractmethod
|
|
261
|
-
async def on_shutdown(self):
|
|
274
|
+
async def on_shutdown(self) -> None:
|
|
262
275
|
"""This method is called when the node is shut down."""
|
|
263
276
|
|
|
264
277
|
@abstractmethod
|
|
265
|
-
async def on_repeat(self):
|
|
278
|
+
async def on_repeat(self) -> None:
|
|
266
279
|
"""This method is called every 10 seconds."""
|
|
267
280
|
|
|
268
281
|
@abstractmethod
|
|
269
|
-
def register_sio_events(self, sio_client: AsyncClient):
|
|
282
|
+
def register_sio_events(self, sio_client: AsyncClient) -> None:
|
|
270
283
|
"""Register (additional) socket.io events for the communication with the learning loop.
|
|
271
284
|
The events: connect, disconnect and restart are already registered and should not be overwritten."""
|
learning_loop_node/rest.py
CHANGED
|
@@ -37,7 +37,7 @@ async def _debug_logging(request: Request) -> str:
|
|
|
37
37
|
@router.put("/socketio")
|
|
38
38
|
async def _socketio(request: Request) -> str:
|
|
39
39
|
'''
|
|
40
|
-
Enable or disable the socketio connection to the learning loop.
|
|
40
|
+
Enable or disable the socketio connection and repeat loop to the learning loop.
|
|
41
41
|
Not intended to be used outside of testing.
|
|
42
42
|
|
|
43
43
|
Example Usage
|
|
@@ -48,7 +48,8 @@ async def _socketio(request: Request) -> str:
|
|
|
48
48
|
node: 'Node' = request.app
|
|
49
49
|
|
|
50
50
|
if state == 'off':
|
|
51
|
-
|
|
51
|
+
if node.sio_client:
|
|
52
|
+
await node.sio_client.disconnect()
|
|
52
53
|
node.set_skip_repeat_loop(True) # Prevent auto-reconnection
|
|
53
54
|
return 'off'
|
|
54
55
|
if state == 'on':
|
|
@@ -46,12 +46,15 @@ def default_user_input() -> UserInput:
|
|
|
46
46
|
@pytest.mark.asyncio
|
|
47
47
|
@pytest.mark.usefixtures('setup_test_project')
|
|
48
48
|
async def test_image_download():
|
|
49
|
+
# pylint: disable=protected-access
|
|
50
|
+
|
|
49
51
|
image_folder = '/tmp/learning_loop_lib_data/zauberzeug/pytest_nodelib_annotator/images'
|
|
50
52
|
|
|
51
53
|
assert os.path.exists(image_folder) is False or len(os.listdir(image_folder)) == 0
|
|
52
54
|
|
|
53
55
|
node = AnnotatorNode(name="", uuid="", annotator_logic=MockedAnnotatatorLogic())
|
|
54
56
|
user_input = default_user_input()
|
|
55
|
-
|
|
57
|
+
await node._ensure_sio_connection() # This is required as the node is not "started"
|
|
58
|
+
_ = await node._handle_user_input(jsonable_encoder(asdict(user_input)))
|
|
56
59
|
|
|
57
60
|
assert os.path.exists(image_folder) is True and len(os.listdir(image_folder)) == 1
|
|
@@ -38,6 +38,15 @@ def should_have_segmentations(request) -> bool:
|
|
|
38
38
|
return should_have_seg
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
@pytest.fixture(scope="session", name="event_loop")
|
|
42
|
+
def fixture_event_loop():
|
|
43
|
+
"""Overrides pytest default function scoped event loop"""
|
|
44
|
+
policy = asyncio.get_event_loop_policy()
|
|
45
|
+
loop = policy.new_event_loop()
|
|
46
|
+
yield loop
|
|
47
|
+
loop.close()
|
|
48
|
+
|
|
49
|
+
|
|
41
50
|
@pytest.fixture()
|
|
42
51
|
async def test_detector_node():
|
|
43
52
|
"""Initializes and runs a detector testnode. Note that the running instance and the one the function returns are not the same instances!"""
|
|
@@ -20,6 +20,30 @@ async def test_outbox():
|
|
|
20
20
|
test_outbox = Outbox()
|
|
21
21
|
|
|
22
22
|
yield test_outbox
|
|
23
|
+
|
|
24
|
+
await test_outbox.set_mode('stopped')
|
|
25
|
+
shutil.rmtree(test_outbox.path, ignore_errors=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(autouse=True, scope='session')
|
|
29
|
+
async def fix_upload_bug():
|
|
30
|
+
""" This is a workaround for an upload bug that causes the SECOND upload to fail on the CI server. """
|
|
31
|
+
os.environ['LOOP_ORGANIZATION'] = 'zauberzeug'
|
|
32
|
+
os.environ['LOOP_PROJECT'] = 'demo'
|
|
33
|
+
shutil.rmtree(f'{GLOBALS.data_folder}/outbox', ignore_errors=True)
|
|
34
|
+
test_outbox = Outbox()
|
|
35
|
+
|
|
36
|
+
await test_outbox.set_mode('continuous_upload')
|
|
37
|
+
await test_outbox.save(get_test_image_binary())
|
|
38
|
+
await asyncio.sleep(6)
|
|
39
|
+
assert await wait_for_outbox_count(test_outbox, 0, timeout=15), 'File was not cleared even though outbox should be in continuous_upload'
|
|
40
|
+
assert test_outbox.upload_counter == 1
|
|
41
|
+
|
|
42
|
+
await test_outbox.save(get_test_image_binary())
|
|
43
|
+
await asyncio.sleep(6)
|
|
44
|
+
# assert await wait_for_outbox_count(test_outbox, 0, timeout=90), 'File was not cleared even though outbox should be in continuous_upload'
|
|
45
|
+
# assert test_outbox.upload_counter == 2
|
|
46
|
+
|
|
23
47
|
await test_outbox.set_mode('stopped')
|
|
24
48
|
shutil.rmtree(test_outbox.path, ignore_errors=True)
|
|
25
49
|
|
|
@@ -37,17 +61,6 @@ async def test_set_outbox_mode(test_outbox: Outbox):
|
|
|
37
61
|
assert test_outbox.upload_counter == 1
|
|
38
62
|
|
|
39
63
|
|
|
40
|
-
@pytest.mark.asyncio
|
|
41
|
-
async def test_outbox_upload_is_successful(test_outbox: Outbox):
|
|
42
|
-
await test_outbox.save(get_test_image_binary())
|
|
43
|
-
await asyncio.sleep(1)
|
|
44
|
-
await test_outbox.save(get_test_image_binary())
|
|
45
|
-
assert await wait_for_outbox_count(test_outbox, 2)
|
|
46
|
-
await test_outbox.upload()
|
|
47
|
-
assert await wait_for_outbox_count(test_outbox, 0)
|
|
48
|
-
assert test_outbox.upload_counter == 2
|
|
49
|
-
|
|
50
|
-
|
|
51
64
|
@pytest.mark.asyncio
|
|
52
65
|
async def test_invalid_jpg_is_not_saved(test_outbox: Outbox):
|
|
53
66
|
invalid_bytes = b'invalid jpg'
|
|
@@ -58,14 +71,13 @@ async def test_invalid_jpg_is_not_saved(test_outbox: Outbox):
|
|
|
58
71
|
# ------------------------------ Helper functions --------------------------------------
|
|
59
72
|
|
|
60
73
|
|
|
61
|
-
def get_test_image_binary():
|
|
62
|
-
img = Image.new('RGB', (
|
|
74
|
+
def get_test_image_binary() -> bytes:
|
|
75
|
+
img = Image.new('RGB', (600, 300), color=(73, 109, 137))
|
|
63
76
|
# convert img to jpg binary
|
|
64
77
|
|
|
65
78
|
img_byte_arr = io.BytesIO()
|
|
66
79
|
img.save(img_byte_arr, format='JPEG')
|
|
67
|
-
|
|
68
|
-
return img_byte_arr
|
|
80
|
+
return img_byte_arr.getvalue()
|
|
69
81
|
|
|
70
82
|
# return img.tobytes() # NOT WORKING
|
|
71
83
|
|
|
@@ -30,10 +30,10 @@ async def test_filter_is_used_by_node(test_detector_node: DetectorNode, autouplo
|
|
|
30
30
|
assert test_detector_node.outbox.path.startswith('/tmp')
|
|
31
31
|
assert len(get_outbox_files(test_detector_node.outbox)) == 0
|
|
32
32
|
|
|
33
|
-
image = np.fromfile(file=test_image_path, dtype=np.uint8)
|
|
34
|
-
_ = await test_detector_node.get_detections(image, '00:.....',
|
|
33
|
+
image = bytes(np.fromfile(file=test_image_path, dtype=np.uint8))
|
|
34
|
+
_ = await test_detector_node.get_detections(image, tags=[], camera_id='00:.....', autoupload=autoupload)
|
|
35
35
|
# NOTE adding second images with identical detections
|
|
36
|
-
_ = await test_detector_node.get_detections(image, '00:.....',
|
|
36
|
+
_ = await test_detector_node.get_detections(image, tags=[], camera_id='00:.....', autoupload=autoupload)
|
|
37
37
|
await asyncio.sleep(.5) # files are stored asynchronously
|
|
38
38
|
|
|
39
39
|
assert len(get_outbox_files(test_detector_node.outbox)) == expected_file_count, \
|
|
@@ -35,7 +35,7 @@ async def test_initialized_trainer_node():
|
|
|
35
35
|
'training_number': 0,
|
|
36
36
|
'model_variant': '',
|
|
37
37
|
'hyperparameters': {
|
|
38
|
-
'resolution':
|
|
38
|
+
'resolution': 832,
|
|
39
39
|
'fliplr': 0.5,
|
|
40
40
|
'flipud': 0.5}
|
|
41
41
|
})
|
|
@@ -58,7 +58,7 @@ async def test_initialized_trainer():
|
|
|
58
58
|
'training_number': 0,
|
|
59
59
|
'model_variant': '',
|
|
60
60
|
'hyperparameters': {
|
|
61
|
-
'resolution':
|
|
61
|
+
'resolution': 832,
|
|
62
62
|
'fliplr': 0.5,
|
|
63
63
|
'flipud': 0.5}
|
|
64
64
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import asyncio
|
|
3
3
|
|
|
4
|
-
from pytest_mock import
|
|
4
|
+
from pytest_mock import ( # pip install pytest-mock # pylint: disable=import-error # type: ignore
|
|
5
|
+
MockerFixture,
|
|
6
|
+
)
|
|
5
7
|
|
|
6
8
|
from ....enums import TrainerState
|
|
7
9
|
from ....trainer.trainer_logic import TrainerLogic
|
|
@@ -54,6 +56,7 @@ async def test_unsynced_model_available__sync_successful(test_initialized_traine
|
|
|
54
56
|
async def test_unsynced_model_available__sio_not_connected(test_initialized_trainer_node: TrainerNode):
|
|
55
57
|
trainer = test_initialized_trainer_node.trainer_logic
|
|
56
58
|
assert isinstance(trainer, TestingTrainerLogic)
|
|
59
|
+
assert test_initialized_trainer_node.sio_client is not None
|
|
57
60
|
|
|
58
61
|
await test_initialized_trainer_node.sio_client.disconnect()
|
|
59
62
|
test_initialized_trainer_node.set_skip_repeat_loop(True)
|
|
@@ -6,13 +6,25 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from dataclasses import asdict
|
|
9
|
-
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, List, Optional
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional
|
|
10
10
|
|
|
11
11
|
from fastapi.encoders import jsonable_encoder
|
|
12
12
|
|
|
13
|
-
from ..data_classes import
|
|
13
|
+
from ..data_classes import (
|
|
14
|
+
Context,
|
|
15
|
+
Errors,
|
|
16
|
+
PretrainedModel,
|
|
17
|
+
Training,
|
|
18
|
+
TrainingOut,
|
|
19
|
+
TrainingStateData,
|
|
20
|
+
TrainingStatus,
|
|
21
|
+
)
|
|
14
22
|
from ..enums import TrainerState
|
|
15
|
-
from ..helpers.misc import
|
|
23
|
+
from ..helpers.misc import (
|
|
24
|
+
create_project_folder,
|
|
25
|
+
delete_all_training_folders,
|
|
26
|
+
is_valid_uuid4,
|
|
27
|
+
)
|
|
16
28
|
from .downloader import TrainingsDownloader
|
|
17
29
|
from .exceptions import CriticalError, NodeNeedsRestartError
|
|
18
30
|
from .io_helpers import ActiveTrainingIO, EnvironmentVars, LastTrainingIO
|
|
@@ -66,7 +78,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
66
78
|
return self._training
|
|
67
79
|
|
|
68
80
|
@property
|
|
69
|
-
def hyperparameters(self) ->
|
|
81
|
+
def hyperparameters(self) -> Dict[str, Any]:
|
|
70
82
|
assert self._training is not None, 'Training should have data'
|
|
71
83
|
return self._training.hyperparameters
|
|
72
84
|
|
|
@@ -357,6 +369,9 @@ class TrainerLogicGeneric(ABC):
|
|
|
357
369
|
"""Syncronizes the training with the Learning Loop via the update_training endpoint.
|
|
358
370
|
NOTE: This stage sets the errors explicitly because it may be used inside the training stage.
|
|
359
371
|
"""
|
|
372
|
+
if not self.node.sio_client or not self.node.sio_client.connected:
|
|
373
|
+
raise ConnectionError('SocketIO client is not connected')
|
|
374
|
+
|
|
360
375
|
error_key = 'sync_confusion_matrix'
|
|
361
376
|
try:
|
|
362
377
|
new_best_model = self._get_new_best_training_state()
|
|
@@ -16,7 +16,7 @@ from .trainer_logic_generic import TrainerLogicGeneric
|
|
|
16
16
|
class TrainerNode(Node):
|
|
17
17
|
|
|
18
18
|
def __init__(self, name: str, trainer_logic: TrainerLogicGeneric, uuid: Optional[str] = None, use_backdoor_controls: bool = False):
|
|
19
|
-
super().__init__(name, uuid, 'trainer')
|
|
19
|
+
super().__init__(name, uuid=uuid, node_type='trainer')
|
|
20
20
|
trainer_logic._node = self
|
|
21
21
|
self.trainer_logic = trainer_logic
|
|
22
22
|
self.last_training_io = LastTrainingIO(self.uuid)
|
|
@@ -52,7 +52,8 @@ class TrainerNode(Node):
|
|
|
52
52
|
self.check_idle_timeout()
|
|
53
53
|
except exceptions.TimeoutError:
|
|
54
54
|
self.log.warning('timeout when sending status to learning loop, reconnecting sio_client')
|
|
55
|
-
|
|
55
|
+
if self.sio_client:
|
|
56
|
+
await self.sio_client.disconnect() # NOTE: reconnect happens in node._on_repeat
|
|
56
57
|
except Exception:
|
|
57
58
|
self.log.exception('could not send status. Exception:')
|
|
58
59
|
|
|
@@ -76,7 +77,7 @@ class TrainerNode(Node):
|
|
|
76
77
|
return True
|
|
77
78
|
|
|
78
79
|
async def send_status(self):
|
|
79
|
-
if not self.sio_client.connected:
|
|
80
|
+
if not self.sio_client or not self.sio_client.connected:
|
|
80
81
|
self.log.debug('cannot send status - not connected to the Learning Loop')
|
|
81
82
|
return
|
|
82
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: learning-loop-node
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
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
|
|
@@ -100,6 +100,8 @@ You can additionally provide the following camera parameters:
|
|
|
100
100
|
- `autoupload`: configures auto-submission to the learning loop; `filtered` (default), `all`, `disabled` (example curl parameter `-H 'autoupload: all'`)
|
|
101
101
|
- `camera-id`: a string which groups images for submission together (example curl parameter `-H 'camera-id: front_cam'`)
|
|
102
102
|
|
|
103
|
+
To use the socketio interface, the caller needs to connect to the detector node's socketio server and emit the `detect` or `batch_detect` event with the image data and image metadata. Example code can be found [in the rosys implementation](https://github.com/zauberzeug/rosys/blob/main/rosys/vision/detector_hardware.py).
|
|
104
|
+
|
|
103
105
|
The detector also has a sio **upload endpoint** that can be used to upload images and detections to the learning loop. The function receives a json dictionary, with the following entries:
|
|
104
106
|
|
|
105
107
|
- `image`: the image data in jpg format
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
learning_loop_node/__init__.py,sha256=onN5s8-x_xBsCM6NLmJO0Ym1sJHeCFaGw8qb0oQZmz8,364
|
|
2
2
|
learning_loop_node/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
learning_loop_node/annotation/annotator_logic.py,sha256=BTaopkJZkIf1CI5lfsVKsxbxoUIbDJrevavuQUT5e_c,1000
|
|
4
|
-
learning_loop_node/annotation/annotator_node.py,sha256=
|
|
5
|
-
learning_loop_node/data_classes/__init__.py,sha256=
|
|
4
|
+
learning_loop_node/annotation/annotator_node.py,sha256=J5xwSnM5rwTWrTe-TI37J0JHKf_4PlDuABaHvgjYr_Q,4443
|
|
5
|
+
learning_loop_node/data_classes/__init__.py,sha256=6-pLbokCAvTFW-lh1lLUu7u8V5ZyD-2IVmFg5HHI4Cc,1329
|
|
6
6
|
learning_loop_node/data_classes/annotations.py,sha256=NfMlTv2_5AfVY_JDM4tbjETFjSN2S2I2LJJPMMcDT50,966
|
|
7
7
|
learning_loop_node/data_classes/detections.py,sha256=7vqcS0EK8cmDjRDckHlpSZDZ9YO6qajRmYvx-oxatFc,5425
|
|
8
|
-
learning_loop_node/data_classes/general.py,sha256=
|
|
9
|
-
learning_loop_node/data_classes/image_metadata.py,sha256=
|
|
8
|
+
learning_loop_node/data_classes/general.py,sha256=GQ6vPEIm4qqBV4RZT_YS_dPeKMdbCKo6Pe5-e4Cg3_k,7295
|
|
9
|
+
learning_loop_node/data_classes/image_metadata.py,sha256=YccDyHMbnOrRr4-9hHbCNBpuhlZem5M64c0ZbZXTASY,1764
|
|
10
10
|
learning_loop_node/data_classes/socket_response.py,sha256=tIdt-oYf6ULoJIDYQCecNM9OtWR6_wJ9tL0Ksu83Vko,655
|
|
11
|
-
learning_loop_node/data_classes/training.py,sha256=
|
|
12
|
-
learning_loop_node/data_exchanger.py,sha256=
|
|
11
|
+
learning_loop_node/data_classes/training.py,sha256=TybwcCDf_NUaDUaOj30lPm-7Z3Qk9XFRibEX5qIv96Y,5737
|
|
12
|
+
learning_loop_node/data_exchanger.py,sha256=nd9JNPLn9amIeTcSIyUPpbE97ORAcb5yNphvmpgWSUQ,9095
|
|
13
13
|
learning_loop_node/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
learning_loop_node/detector/detector_logic.py,sha256=
|
|
15
|
-
learning_loop_node/detector/detector_node.py,sha256=
|
|
14
|
+
learning_loop_node/detector/detector_logic.py,sha256=YmsEsqSr0CUUWKtSR7EFU92HA90NvdYiPZGDQKXJUxU,2462
|
|
15
|
+
learning_loop_node/detector/detector_node.py,sha256=IW9vGbl8Xq7DdylYM-jSJtitkCTs4uGYRZyWGuWauYo,29498
|
|
16
16
|
learning_loop_node/detector/exceptions.py,sha256=C6KbNPlSbtfgDrZx2Hbhm7Suk9jVoR3fMRCO0CkrMsQ,196
|
|
17
17
|
learning_loop_node/detector/inbox_filter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
learning_loop_node/detector/inbox_filter/cam_observation_history.py,sha256=1PHgXRrhSQ34HSFw7mdX8ndRxHf_i1aP5nXXnrZxhAY,3312
|
|
19
19
|
learning_loop_node/detector/inbox_filter/relevance_filter.py,sha256=rI46jL9ZuI0hiDVxWCfXllB8DlQyyewNs6oZ6MnglMc,1540
|
|
20
|
-
learning_loop_node/detector/outbox.py,sha256=
|
|
20
|
+
learning_loop_node/detector/outbox.py,sha256=izWJtnHG0PNX3-YWtkybLch2slnmT2pmAYrqZpHOaTA,12768
|
|
21
21
|
learning_loop_node/detector/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
learning_loop_node/detector/rest/about.py,sha256=evHJ2svUZY_DFz0FSef5u9c5KW4Uc3GL7EbPinG9-dg,583
|
|
23
23
|
learning_loop_node/detector/rest/backdoor_controls.py,sha256=ZNaFOvC0OLWNtcLiG-NIqS_y1kkLP4csgk3CHhp8Gis,885
|
|
@@ -41,16 +41,16 @@ learning_loop_node/helpers/log_conf.py,sha256=hqVAa_9NnYEU6N0dcOKmph82p7MpgKqeF_
|
|
|
41
41
|
learning_loop_node/helpers/misc.py,sha256=J29iBmsEUAraKKDN1m1NKiHQ3QrP5ub5HBU6cllSP2g,7384
|
|
42
42
|
learning_loop_node/helpers/run.py,sha256=_uox-j3_K_bL3yCAwy3JYSOiIxrnhzVxyxWpCe8_J9U,876
|
|
43
43
|
learning_loop_node/loop_communication.py,sha256=opulqBKRLXlUQgjA3t0pg8CNA-JXJRCPPUspRxRuuGw,7556
|
|
44
|
-
learning_loop_node/node.py,sha256
|
|
44
|
+
learning_loop_node/node.py,sha256=xK-xODRo7ov-dNNMcpLW2GAauvjKAK3K9RQh4P9S994,12160
|
|
45
45
|
learning_loop_node/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
learning_loop_node/rest.py,sha256=
|
|
46
|
+
learning_loop_node/rest.py,sha256=5X9IVW9kf1gNf8jifGW9g_gI_-9TEeoMMOW16jvwpRE,1599
|
|
47
47
|
learning_loop_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
48
|
learning_loop_node/tests/annotator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
learning_loop_node/tests/annotator/conftest.py,sha256=e83I8WNAUgCFmum1GCx_nSjP9uwAoPIwPk72elypNQY,2098
|
|
50
50
|
learning_loop_node/tests/annotator/pytest.ini,sha256=8QdjmawLy1zAzXrJ88or1kpFDhJw0W5UOnDfGGs_igU,262
|
|
51
|
-
learning_loop_node/tests/annotator/test_annotator_node.py,sha256=
|
|
51
|
+
learning_loop_node/tests/annotator/test_annotator_node.py,sha256=OgdUj0PEWSe0KPTNVVi-1d7DoK7IC9Q3Q3G8TPiP9f4,2090
|
|
52
52
|
learning_loop_node/tests/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
learning_loop_node/tests/detector/conftest.py,sha256=
|
|
53
|
+
learning_loop_node/tests/detector/conftest.py,sha256=Z1uPZGSL5jZyRQkHycQpHjsBjn-sL1QfuJrrJrGTNtM,5517
|
|
54
54
|
learning_loop_node/tests/detector/inbox_filter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
55
|
learning_loop_node/tests/detector/inbox_filter/test_observation.py,sha256=k4WYdvnuV7d_r7zI4M2aA8WuBjm0aycQ0vj1rGE2q4w,1370
|
|
56
56
|
learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py,sha256=r-wABFQVsTNTjv7vYGr8wbHfOWy43F_B14ZDWHfiZ-A,7613
|
|
@@ -59,8 +59,8 @@ learning_loop_node/tests/detector/pytest.ini,sha256=8QdjmawLy1zAzXrJ88or1kpFDhJw
|
|
|
59
59
|
learning_loop_node/tests/detector/test.jpg,sha256=msA-vHPmvPiro_D102Qmn1fn4vNfooqYYEXPxZUmYpk,161390
|
|
60
60
|
learning_loop_node/tests/detector/test_client_communication.py,sha256=cVviUmAwbLY3LsJcY-D3ve-Jwxk9WVOrVupeh-PdKtA,8013
|
|
61
61
|
learning_loop_node/tests/detector/test_detector_node.py,sha256=0ZMV6coAvdq-nH8CwY9_LR2tUcH9VLcAB1CWuwHQMpo,3023
|
|
62
|
-
learning_loop_node/tests/detector/test_outbox.py,sha256=
|
|
63
|
-
learning_loop_node/tests/detector/test_relevance_filter.py,sha256=
|
|
62
|
+
learning_loop_node/tests/detector/test_outbox.py,sha256=K7c0GeKujNlgjDFS3aY1lN7kDbfJ4dBQfB9lBp3o3_Q,3262
|
|
63
|
+
learning_loop_node/tests/detector/test_relevance_filter.py,sha256=7oTXW4AuObk7NxMqGSwnjcspH3-QUbSdCYlz9hvzV78,2079
|
|
64
64
|
learning_loop_node/tests/detector/testing_detector.py,sha256=MZajybyzISz2G1OENfLHgZhBcLCYzTR4iN9JkWpq5-s,551
|
|
65
65
|
learning_loop_node/tests/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
66
|
learning_loop_node/tests/general/conftest.py,sha256=kEtkuVA2wgny-YBkLDn7Ff5j6ShOPghQUU0cH9IIl_8,2430
|
|
@@ -73,7 +73,7 @@ learning_loop_node/tests/general/test_downloader.py,sha256=y4GcUyR0OAfrwltd6eyQg
|
|
|
73
73
|
learning_loop_node/tests/general/test_learning_loop_node.py,sha256=SZd-VChpWnnsPN46pr4E_LL3ZevYx6psU-AWdVeOFpQ,770
|
|
74
74
|
learning_loop_node/tests/test_helper.py,sha256=Xajn6BWJqeD36YAETwdcJd6awY2NPmaOis3gWgFc97k,2909
|
|
75
75
|
learning_loop_node/tests/trainer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
|
-
learning_loop_node/tests/trainer/conftest.py,sha256=
|
|
76
|
+
learning_loop_node/tests/trainer/conftest.py,sha256=eJUUBVRTmwcEooEN29hIa3eNuo0ogAPNn7Vqs9FSRDM,3660
|
|
77
77
|
learning_loop_node/tests/trainer/pytest.ini,sha256=8QdjmawLy1zAzXrJ88or1kpFDhJw0W5UOnDfGGs_igU,262
|
|
78
78
|
learning_loop_node/tests/trainer/state_helper.py,sha256=MDe9opeKruip74FoRFff8MSWGiQNFqDpPtIEIbgPnFc,919
|
|
79
79
|
learning_loop_node/tests/trainer/states/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -81,7 +81,7 @@ learning_loop_node/tests/trainer/states/test_state_cleanup.py,sha256=gZNxSSwnj9f
|
|
|
81
81
|
learning_loop_node/tests/trainer/states/test_state_detecting.py,sha256=-NLR5se7_OY_X8_Gf-BWw7X6dS_Pzsnkz84J5aTbqFU,3689
|
|
82
82
|
learning_loop_node/tests/trainer/states/test_state_download_train_model.py,sha256=-T8iAutBliv0MV5bV5lPvn2aNjF3vMBCj8iAZTC-Q7g,2992
|
|
83
83
|
learning_loop_node/tests/trainer/states/test_state_prepare.py,sha256=boCU93Bv2VWbW73MC_suTbwCcuR7RWn-6dgVvdiJ9tA,2291
|
|
84
|
-
learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py,sha256=
|
|
84
|
+
learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py,sha256=R3UqQJ2GQMapwRQ5WuZJb9M5IfroD2QqFI4h8etiH0Y,5223
|
|
85
85
|
learning_loop_node/tests/trainer/states/test_state_train.py,sha256=ovRs8EepQjy0yQJssK0TdcZcraBhmUkbMWeNKdHS114,2893
|
|
86
86
|
learning_loop_node/tests/trainer/states/test_state_upload_detections.py,sha256=oFQGTeRZhW7MBISAfpe65KphZNxFUsZu3-5hD9_LS6k,7438
|
|
87
87
|
learning_loop_node/tests/trainer/states/test_state_upload_model.py,sha256=jHWLa48tNljZwIiqI-1z71ENRGnn7Z0BsVcDBVWVBj4,3642
|
|
@@ -97,8 +97,8 @@ learning_loop_node/trainer/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
|
97
97
|
learning_loop_node/trainer/rest/backdoor_controls.py,sha256=ZnK8ypY5r_q0-YZbtaOxhQThzuZvMsQHM5gJGESd_dE,5131
|
|
98
98
|
learning_loop_node/trainer/test_executor.py,sha256=6BVGDN_6f5GEMMEvDLSG1yzMybSvgXaP5uYpSfsVPP0,2224
|
|
99
99
|
learning_loop_node/trainer/trainer_logic.py,sha256=eK-01qZzi10UjLMCQX8vy5eW2FoghPj3rzzDC-s3Si4,8792
|
|
100
|
-
learning_loop_node/trainer/trainer_logic_generic.py,sha256=
|
|
101
|
-
learning_loop_node/trainer/trainer_node.py,sha256=
|
|
102
|
-
learning_loop_node-0.
|
|
103
|
-
learning_loop_node-0.
|
|
104
|
-
learning_loop_node-0.
|
|
100
|
+
learning_loop_node/trainer/trainer_logic_generic.py,sha256=KcHmXr-Hp8_Wuejzj8odY6sRPqi6aw1SEXv3YlbjM98,27057
|
|
101
|
+
learning_loop_node/trainer/trainer_node.py,sha256=tsAMzJewdS7Bi_1b9FwG0d2lGlv2lY37pgOLWr0bP_I,4582
|
|
102
|
+
learning_loop_node-0.16.0.dist-info/METADATA,sha256=z8fX3WJhdBUbBVFTSC0tXu1wb-t4M1777nshv_k3u6Y,13509
|
|
103
|
+
learning_loop_node-0.16.0.dist-info/WHEEL,sha256=WGfLGfLX43Ei_YORXSnT54hxFygu34kMpcQdmgmEwCQ,88
|
|
104
|
+
learning_loop_node-0.16.0.dist-info/RECORD,,
|
|
File without changes
|