learning-loop-node 0.10.8__tar.gz → 0.10.9__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.

Files changed (97) hide show
  1. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/PKG-INFO +1 -1
  2. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/detector_node.py +1 -1
  3. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/outbox.py +65 -33
  4. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/outbox_mode.py +1 -1
  5. learning_loop_node-0.10.9/learning_loop_node/tests/annotator/conftest.py +50 -0
  6. {learning_loop_node-0.10.8/learning_loop_node/annotation/tests → learning_loop_node-0.10.9/learning_loop_node/tests/annotator}/test_annotator_node.py +9 -11
  7. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/conftest.py +30 -4
  8. {learning_loop_node-0.10.8/learning_loop_node/detector/inbox_filter/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector/inbox_filter}/test_observation.py +1 -1
  9. {learning_loop_node-0.10.8/learning_loop_node/detector/inbox_filter/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector/inbox_filter}/test_relevance_group.py +2 -7
  10. {learning_loop_node-0.10.8/learning_loop_node/detector/inbox_filter/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector/inbox_filter}/test_unexpected_observations_count.py +3 -6
  11. learning_loop_node-0.10.9/learning_loop_node/tests/detector/pytest.ini +10 -0
  12. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/test_client_communication.py +12 -9
  13. learning_loop_node-0.10.9/learning_loop_node/tests/detector/test_outbox.py +96 -0
  14. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/test_relevance_filter.py +8 -6
  15. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/testing_detector.py +3 -3
  16. learning_loop_node-0.10.9/learning_loop_node/tests/general/conftest.py +62 -0
  17. learning_loop_node-0.10.9/learning_loop_node/tests/general/pytest.ini +10 -0
  18. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/test_data_classes.py +3 -3
  19. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/test_downloader.py +24 -12
  20. learning_loop_node-0.10.9/learning_loop_node/tests/general/test_learning_loop_node.py +20 -0
  21. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/tests/test_helper.py +20 -9
  22. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/conftest.py +32 -3
  23. learning_loop_node-0.10.9/learning_loop_node/tests/trainer/pytest.ini +10 -0
  24. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/state_helper.py +2 -1
  25. learning_loop_node-0.10.9/learning_loop_node/tests/trainer/states/__init__.py +0 -0
  26. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_cleanup.py +2 -2
  27. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_detecting.py +5 -5
  28. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_download_train_model.py +3 -3
  29. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_prepare.py +4 -4
  30. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_sync_confusion_matrix.py +3 -4
  31. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_train.py +4 -4
  32. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_upload_detections.py +6 -6
  33. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/states/test_state_upload_model.py +4 -4
  34. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/test_errors.py +3 -3
  35. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/test_trainer_states.py +4 -4
  36. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/testing_trainer_logic.py +2 -2
  37. learning_loop_node-0.10.9/learning_loop_node/trainer/__init__.py +0 -0
  38. learning_loop_node-0.10.9/learning_loop_node/trainer/rest/__init__.py +0 -0
  39. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/trainer}/test_executor.py +1 -1
  40. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/trainer_node.py +5 -7
  41. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/pyproject.toml +1 -1
  42. learning_loop_node-0.10.8/learning_loop_node/conftest.py +0 -89
  43. learning_loop_node-0.10.8/learning_loop_node/detector/tests/test_outbox.py +0 -86
  44. learning_loop_node-0.10.8/learning_loop_node/tests/conftest.py +0 -21
  45. learning_loop_node-0.10.8/learning_loop_node/tests/test_learning_loop_node.py +0 -18
  46. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/README.md +0 -0
  47. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/__init__.py +0 -0
  48. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/annotation/__init__.py +0 -0
  49. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/annotation/annotator_logic.py +0 -0
  50. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/annotation/annotator_node.py +0 -0
  51. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/__init__.py +0 -0
  52. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/annotations.py +0 -0
  53. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/detections.py +0 -0
  54. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/general.py +0 -0
  55. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/socket_response.py +0 -0
  56. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_classes/training.py +0 -0
  57. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/data_exchanger.py +0 -0
  58. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/__init__.py +0 -0
  59. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/detector_logic.py +0 -0
  60. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  61. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  62. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  63. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/__init__.py +0 -0
  64. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/about.py +0 -0
  65. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  66. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/detect.py +0 -0
  67. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  68. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/detector/rest/upload.py +0 -0
  69. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  70. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/globals.py +0 -0
  71. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/helpers}/__init__.py +0 -0
  72. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/helpers/environment_reader.py +0 -0
  73. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  74. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/helpers/log_conf.py +0 -0
  75. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/helpers/misc.py +0 -0
  76. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/loop_communication.py +0 -0
  77. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/node.py +0 -0
  78. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/py.typed +0 -0
  79. {learning_loop_node-0.10.8/learning_loop_node/helpers → learning_loop_node-0.10.9/learning_loop_node/tests}/__init__.py +0 -0
  80. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/annotator}/__init__.py +0 -0
  81. {learning_loop_node-0.10.8/learning_loop_node → learning_loop_node-0.10.9/learning_loop_node/tests/annotator}/pytest.ini +0 -0
  82. {learning_loop_node-0.10.8/learning_loop_node/trainer → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/__init__.py +0 -0
  83. {learning_loop_node-0.10.8/learning_loop_node/trainer/rest → learning_loop_node-0.10.9/learning_loop_node/tests/detector/inbox_filter}/__init__.py +0 -0
  84. {learning_loop_node-0.10.8/learning_loop_node/detector/tests → learning_loop_node-0.10.9/learning_loop_node/tests/detector}/test.jpg +0 -0
  85. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/__init__.py +0 -0
  86. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/test_data/file_1.txt +0 -0
  87. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/test_data/file_2.txt +0 -0
  88. {learning_loop_node-0.10.8/learning_loop_node/tests → learning_loop_node-0.10.9/learning_loop_node/tests/general}/test_data/model.json +0 -0
  89. {learning_loop_node-0.10.8/learning_loop_node/trainer/tests/states → learning_loop_node-0.10.9/learning_loop_node/tests/trainer}/__init__.py +0 -0
  90. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/downloader.py +0 -0
  91. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/exceptions.py +0 -0
  92. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/executor.py +0 -0
  93. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/io_helpers.py +0 -0
  94. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  95. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/rest/controls.py +0 -0
  96. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/trainer_logic.py +0 -0
  97. {learning_loop_node-0.10.8 → learning_loop_node-0.10.9}/learning_loop_node/trainer/trainer_logic_generic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: learning-loop-node
3
- Version: 0.10.8
3
+ Version: 0.10.9
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
@@ -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 time
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 threading import Thread
14
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.upload_process: Optional[Thread] = None
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.upload_process = Thread(target=self._continuous_upload, name='OutboxUpload')
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
- time.sleep(5)
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.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')
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]):
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
+
111
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
- response = requests.post(self.target_uri, files=data, timeout=self.UPLOAD_TIMEOUT_S)
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 ensure_continuous_upload_stopped(self) -> bool:
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
- proc = self.upload_process
138
- if not proc:
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
- assert proc is not None
145
- proc.join(self.UPLOAD_TIMEOUT_S + 1)
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 thread: ')
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.upload_process and self.upload_process.is_alive())
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.upload_process and self.upload_process.is_alive():
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: Union[OutboxMode, str]) -> None:
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 learning_loop_node.annotation.annotator_logic import AnnotatorLogic
10
- from learning_loop_node.annotation.annotator_node import AnnotatorNode
11
- from learning_loop_node.data_classes import (AnnotationData, AnnotationEventType, Category, CategoryType, Context,
12
- Point, ToolOutput, UserInput)
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='pytest_p'),
31
- image_uuid='f786350c-89ca-9424-9b00-720a9a85fe09',
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
- # 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'
41
+ image_folder = '/tmp/learning_loop_lib_data/zauberzeug/pytest_nodelib_annotator/images'
44
42
 
45
- assert os.path.exists(image_path) is False
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(image_path) is True
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 learning_loop_node import DetectorNode
15
- from learning_loop_node.detector.outbox import Outbox
16
- from learning_loop_node.globals import GLOBALS
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)
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  from datetime import datetime, timedelta
3
3
 
4
- from learning_loop_node.data_classes import BoxDetection, Observation
4
+ from ....data_classes import BoxDetection, Observation
5
5
 
6
6
 
7
7
  def test_aging():
@@ -5,13 +5,8 @@ from typing import List
5
5
 
6
6
  from dacite import from_dict
7
7
 
8
- from learning_loop_node.data_classes.detections import (BoxDetection,
9
- Detections, Point,
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 learning_loop_node.data_classes.detections import (BoxDetection,
7
- Detections,
8
- PointDetection)
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 learning_loop_node import DetectorNode
8
- from learning_loop_node.data_classes import ModelInformation
9
- from learning_loop_node.detector.tests.conftest import get_outbox_files
10
- from learning_loop_node.globals import GLOBALS
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('detector/tests/test.jpg', 'rb') as f:
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('detector/tests/test.jpg', 'rb'))}
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('detector/tests/test.jpg', 'rb'))}
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('detector/tests/test.jpg', 'rb') as f:
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 learning_loop_node import DetectorNode
7
- from learning_loop_node.data_classes import (BoxDetection, Detections,
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('detector/tests/test.jpg', np.uint8)
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 learning_loop_node import DetectorLogic
6
- from learning_loop_node.conftest import get_dummy_detections
7
- from learning_loop_node.data_classes import Detections
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):