learning-loop-node 0.15.0__tar.gz → 0.16.1__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 (104) hide show
  1. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/PKG-INFO +1 -1
  2. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/annotation/annotator_node.py +7 -1
  3. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/__init__.py +32 -6
  4. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/general.py +10 -11
  5. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/detector_node.py +145 -134
  6. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/outbox.py +2 -2
  7. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/loop_communication.py +9 -8
  8. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/node.py +49 -36
  9. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/rest.py +3 -2
  10. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/annotator/test_annotator_node.py +4 -1
  11. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/conftest.py +9 -0
  12. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/test_outbox.py +27 -15
  13. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_sync_confusion_matrix.py +4 -1
  14. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/trainer_logic_generic.py +3 -0
  15. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/trainer_node.py +4 -3
  16. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/pyproject.toml +1 -1
  17. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/README.md +0 -0
  18. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/__init__.py +0 -0
  19. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/annotation/__init__.py +0 -0
  20. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/annotation/annotator_logic.py +0 -0
  21. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/annotations.py +0 -0
  22. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/detections.py +0 -0
  23. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/image_metadata.py +0 -0
  24. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/socket_response.py +0 -0
  25. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_classes/training.py +0 -0
  26. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/data_exchanger.py +0 -0
  27. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/__init__.py +0 -0
  28. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/detector_logic.py +0 -0
  29. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/exceptions.py +0 -0
  30. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  31. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
  32. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  33. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/__init__.py +0 -0
  34. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/about.py +0 -0
  35. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  36. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/detect.py +0 -0
  37. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/model_version_control.py +0 -0
  38. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  39. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
  40. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/detector/rest/upload.py +0 -0
  41. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/enums/__init__.py +0 -0
  42. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/enums/annotator.py +0 -0
  43. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/enums/detector.py +0 -0
  44. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/enums/general.py +0 -0
  45. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/enums/trainer.py +0 -0
  46. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  47. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/globals.py +0 -0
  48. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/__init__.py +0 -0
  49. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/background_tasks.py +0 -0
  50. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/environment_reader.py +0 -0
  51. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
  52. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/log_conf.py +0 -0
  53. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/misc.py +0 -0
  54. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/helpers/run.py +0 -0
  55. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/py.typed +0 -0
  56. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/__init__.py +0 -0
  57. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/annotator/__init__.py +0 -0
  58. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/annotator/conftest.py +0 -0
  59. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/annotator/pytest.ini +0 -0
  60. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/__init__.py +0 -0
  61. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/inbox_filter/__init__.py +0 -0
  62. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/inbox_filter/test_observation.py +0 -0
  63. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py +0 -0
  64. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_count.py +0 -0
  65. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/pytest.ini +0 -0
  66. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/test.jpg +0 -0
  67. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/test_client_communication.py +0 -0
  68. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/test_detector_node.py +0 -0
  69. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/test_relevance_filter.py +0 -0
  70. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/detector/testing_detector.py +0 -0
  71. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/__init__.py +0 -0
  72. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/conftest.py +0 -0
  73. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/pytest.ini +0 -0
  74. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_data/file_1.txt +0 -0
  75. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_data/file_2.txt +0 -0
  76. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_data/model.json +0 -0
  77. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_data_classes.py +0 -0
  78. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_downloader.py +0 -0
  79. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/general/test_learning_loop_node.py +0 -0
  80. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/test_helper.py +0 -0
  81. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/__init__.py +0 -0
  82. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/conftest.py +0 -0
  83. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/pytest.ini +0 -0
  84. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/state_helper.py +0 -0
  85. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/__init__.py +0 -0
  86. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_cleanup.py +0 -0
  87. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_detecting.py +0 -0
  88. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_download_train_model.py +0 -0
  89. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_prepare.py +0 -0
  90. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_train.py +0 -0
  91. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_upload_detections.py +0 -0
  92. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/states/test_state_upload_model.py +0 -0
  93. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/test_errors.py +0 -0
  94. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/test_trainer_states.py +0 -0
  95. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/tests/trainer/testing_trainer_logic.py +0 -0
  96. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/__init__.py +0 -0
  97. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/downloader.py +0 -0
  98. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/exceptions.py +0 -0
  99. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/executor.py +0 -0
  100. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/io_helpers.py +0 -0
  101. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/rest/__init__.py +0 -0
  102. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
  103. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/test_executor.py +0 -0
  104. {learning_loop_node-0.15.0 → learning_loop_node-0.16.1}/learning_loop_node/trainer/trainer_logic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: learning-loop-node
3
- Version: 0.15.0
3
+ Version: 0.16.1
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
@@ -18,7 +18,7 @@ from .annotator_logic import AnnotatorLogic
18
18
  class AnnotatorNode(Node):
19
19
 
20
20
  def __init__(self, name: str, annotator_logic: AnnotatorLogic, uuid: Optional[str] = None):
21
- super().__init__(name, uuid, 'annotation_node')
21
+ super().__init__(name, uuid=uuid, node_type='annotation_node')
22
22
  self.tool = annotator_logic
23
23
  self.histories: Dict = {}
24
24
  annotator_logic.init(self)
@@ -35,6 +35,9 @@ class AnnotatorNode(Node):
35
35
  return self.tool.logout_user(sid)
36
36
 
37
37
  async def _handle_user_input(self, user_input_dict: Dict) -> str:
38
+ if not self.sio_client or not self.sio_client.connected:
39
+ raise ConnectionError('SocketIO client is not connected')
40
+
38
41
  user_input = from_dict(data_class=UserInput, data=user_input_dict)
39
42
 
40
43
  if user_input.data.key_up == 'Escape':
@@ -66,6 +69,9 @@ class AnnotatorNode(Node):
66
69
 
67
70
  async def send_status(self):
68
71
 
72
+ if not self.sio_client or not self.sio_client.connected:
73
+ raise ConnectionError('SocketIO client is not connected')
74
+
69
75
  status = AnnotationNodeStatus(
70
76
  id=self.uuid,
71
77
  name=self.name,
@@ -1,17 +1,43 @@
1
1
  from .annotations import AnnotationData, SegmentationAnnotation, ToolOutput, UserInput
2
- from .detections import (BoxDetection, ClassificationDetection, Detections, Observation, Point, PointDetection,
3
- SegmentationDetection, Shape)
4
- from .general import (AboutResponse, AnnotationNodeStatus, Category, Context, DetectionStatus, ErrorConfiguration,
5
- ModelInformation, ModelVersionResponse, NodeState, NodeStatus)
2
+ from .detections import (
3
+ BoxDetection,
4
+ ClassificationDetection,
5
+ Detections,
6
+ Observation,
7
+ Point,
8
+ PointDetection,
9
+ SegmentationDetection,
10
+ Shape,
11
+ )
12
+ from .general import (
13
+ AboutResponse,
14
+ AnnotationNodeStatus,
15
+ Category,
16
+ Context,
17
+ DetectorStatus,
18
+ ErrorConfiguration,
19
+ ModelInformation,
20
+ ModelVersionResponse,
21
+ NodeState,
22
+ NodeStatus,
23
+ )
6
24
  from .image_metadata import ImageMetadata, ImagesMetadata
7
25
  from .socket_response import SocketResponse
8
- from .training import Errors, PretrainedModel, Training, TrainingError, TrainingOut, TrainingStateData, TrainingStatus
26
+ from .training import (
27
+ Errors,
28
+ PretrainedModel,
29
+ Training,
30
+ TrainingError,
31
+ TrainingOut,
32
+ TrainingStateData,
33
+ TrainingStatus,
34
+ )
9
35
 
10
36
  __all__ = [
11
37
  'AboutResponse', 'AnnotationData', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
12
38
  'BoxDetection', 'ClassificationDetection', 'ImageMetadata', 'Observation', 'Point', 'PointDetection',
13
39
  'SegmentationDetection', 'Shape', 'Detections',
14
- 'AnnotationNodeStatus', 'Category', 'Context', 'DetectionStatus', 'ErrorConfiguration',
40
+ 'AnnotationNodeStatus', 'Category', 'Context', 'DetectorStatus', 'ErrorConfiguration',
15
41
  'ModelInformation', 'NodeState', 'NodeStatus', 'ModelVersionResponse', 'ImagesMetadata',
16
42
  'SocketResponse',
17
43
  'Errors', 'PretrainedModel', 'Training',
@@ -148,8 +148,8 @@ class NodeState(str, Enum):
148
148
  class NodeStatus():
149
149
  id: str
150
150
  name: str
151
- state: Optional[NodeState] = NodeState.Online
152
- uptime: Optional[int] = 0
151
+ state: NodeState = NodeState.Online
152
+ uptime: int = 0
153
153
  errors: Dict = field(default_factory=dict)
154
154
  capabilities: List[str] = field(default_factory=list)
155
155
 
@@ -175,14 +175,13 @@ class AnnotationNodeStatus(NodeStatus):
175
175
 
176
176
 
177
177
  @dataclass(**KWONLY_SLOTS)
178
- class DetectionStatus():
179
- id: str
178
+ class DetectorStatus():
179
+ uuid: str
180
180
  name: str
181
+ state: NodeState
182
+ uptime: int
181
183
  model_format: str
182
-
183
- state: Optional[NodeState] = None
184
- errors: Optional[Dict] = None
185
- uptime: Optional[int] = None
186
- current_model: Optional[str] = None
187
- target_model: Optional[str] = None
188
- operation_mode: Optional[str] = None
184
+ current_model: Optional[str]
185
+ target_model: Optional[str]
186
+ errors: Dict
187
+ operation_mode: str
@@ -13,9 +13,17 @@ from dacite import from_dict
13
13
  from fastapi.encoders import jsonable_encoder
14
14
  from socketio import AsyncClient
15
15
 
16
- from ..data_classes import (AboutResponse, Category, Context, DetectionStatus, ImageMetadata, ImagesMetadata,
17
- ModelInformation, ModelVersionResponse, Shape)
18
- from ..data_classes.socket_response import SocketResponse
16
+ from ..data_classes import (
17
+ AboutResponse,
18
+ Category,
19
+ Context,
20
+ DetectorStatus,
21
+ ImageMetadata,
22
+ ImagesMetadata,
23
+ ModelInformation,
24
+ ModelVersionResponse,
25
+ Shape,
26
+ )
19
27
  from ..data_exchanger import DataExchanger, DownloadError
20
28
  from ..enums import OperationMode, VersionMode
21
29
  from ..globals import GLOBALS
@@ -37,7 +45,7 @@ from .rest import upload as rest_upload
37
45
  class DetectorNode(Node):
38
46
 
39
47
  def __init__(self, name: str, detector: DetectorLogic, uuid: Optional[str] = None, use_backdoor_controls: bool = False) -> None:
40
- super().__init__(name, uuid, 'detector', False)
48
+ super().__init__(name, uuid=uuid, node_type='detector', needs_login=False, needs_sio=False)
41
49
  self.detector_logic = detector
42
50
  self.organization = environment_reader.organization()
43
51
  self.project = environment_reader.project()
@@ -64,6 +72,10 @@ class DetectorNode(Node):
64
72
  self.target_model: Optional[ModelInformation] = None
65
73
  self.loop_deployment_target: Optional[ModelInformation] = None
66
74
 
75
+ self._regular_status_sync_cycles: int = int(os.environ.get('SYNC_CYCLES', '6'))
76
+ """sync status every 6 cycles (6*10s = 1min)"""
77
+ self._repeat_cycles_to_next_sync: int = 0
78
+
67
79
  self.include_router(rest_detect.router, tags=["detect"])
68
80
  self.include_router(rest_upload.router, prefix="")
69
81
  self.include_router(rest_mode.router, tags=["operation_mode"])
@@ -74,7 +86,7 @@ class DetectorNode(Node):
74
86
  if use_backdoor_controls or os.environ.get('USE_BACKDOOR_CONTROLS', '0').lower() in ('1', 'true'):
75
87
  self.include_router(backdoor_controls.router)
76
88
 
77
- self.setup_sio_server()
89
+ self._setup_sio_server()
78
90
 
79
91
  def get_about_response(self) -> AboutResponse:
80
92
  return AboutResponse(
@@ -190,13 +202,7 @@ class DetectorNode(Node):
190
202
  except Exception:
191
203
  self.log.exception("error during 'shutdown'")
192
204
 
193
- async def on_repeat(self) -> None:
194
- try:
195
- await self._check_for_update()
196
- except Exception:
197
- self.log.exception("error during '_check_for_update'")
198
-
199
- def setup_sio_server(self) -> None:
205
+ def _setup_sio_server(self) -> None:
200
206
  """The DetectorNode acts as a SocketIO server. This method sets up the server and defines the event handlers."""
201
207
  # pylint: disable=unused-argument
202
208
 
@@ -322,96 +328,22 @@ class DetectorNode(Node):
322
328
  def connect(sid, environ, auth) -> None:
323
329
  self.connected_clients.append(sid)
324
330
 
325
- async def _check_for_update(self) -> None:
326
- try:
327
- self.log.debug('Current operation mode is %s', self.operation_mode)
328
- try:
329
- await self.sync_status_with_learning_loop()
330
- except Exception:
331
- self.log.exception('Sync with learning loop failed (could not check for updates):')
332
- return
333
-
334
- if self.operation_mode != OperationMode.Idle:
335
- self.log.debug('not checking for updates; operation mode is %s', self.operation_mode)
336
- return
337
-
338
- self.status.reset_error('update_model')
339
- if self.target_model is None:
340
- self.log.debug('not checking for updates; no target model selected')
341
- return
342
-
343
- if self.detector_logic.model_info is not None:
344
- current_version = self.detector_logic.model_info.version
345
- else:
346
- current_version = None
331
+ # ================================== Repeat Cycle, sync and model updates ==================================
347
332
 
348
- if current_version != self.target_model.version:
349
- self.log.info('Current model "%s" needs to be updated to %s',
350
- current_version or "-", self.target_model.version)
351
-
352
- with step_into(GLOBALS.data_folder):
353
- model_symlink = 'model'
354
- target_model_folder = f'models/{self.target_model.version}'
355
- if os.path.exists(target_model_folder) and len(os.listdir(target_model_folder)) > 0:
356
- self.log.info('No need to download model %s (already exists)', self.target_model.version)
357
- else:
358
- os.makedirs(target_model_folder, exist_ok=True)
359
- try:
360
- await self.data_exchanger.download_model(target_model_folder,
361
- Context(organization=self.organization,
362
- project=self.project),
363
- self.target_model.id,
364
- self.detector_logic.model_format)
365
- self.log.info('Downloaded model %s', self.target_model.version)
366
- except Exception:
367
- self.log.exception('Could not download model %s', self.target_model.version)
368
- shutil.rmtree(target_model_folder, ignore_errors=True)
369
- return
370
- try:
371
- os.unlink(model_symlink)
372
- os.remove(model_symlink)
373
- except Exception:
374
- pass
375
- os.symlink(target_model_folder, model_symlink)
376
- self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
377
-
378
- try:
379
- self.detector_logic.load_model_info_and_init_model()
380
- except NodeNeedsRestartError:
381
- self.log.error('Node needs restart')
382
- sys.exit(0)
383
- except Exception:
384
- self.log.exception('Could not load model, will retry download on next check')
385
- shutil.rmtree(target_model_folder, ignore_errors=True)
386
- return
387
- try:
388
- await self.sync_status_with_learning_loop()
389
- except Exception:
390
- pass
391
- # self.reload(reason='new model installed')
392
-
393
- except Exception as e:
394
- self.log.exception('check_for_update failed')
395
- msg = e.cause if isinstance(e, DownloadError) else str(e)
396
- self.status.set_error('update_model', f'Could not update model: {msg}')
397
- try:
398
- await self.sync_status_with_learning_loop()
399
- except Exception:
400
- pass
401
-
402
- async def sync_status_with_learning_loop(self) -> None:
403
- """Sync status of the detector with the Learning Loop.
404
- The Learning Loop will respond with the model info of the deployment target.
405
- If version_control is set to FollowLoop, the detector will update the target_model.
406
- Return if the communication was successful.
407
-
408
- Raises:
409
- Exception: If the communication with the Learning Loop failed.
410
- """
333
+ async def on_repeat(self) -> None:
334
+ """Implementation of the repeat cycle. This method is called every 10 seconds.
335
+ To avoid too many requests, the status is only synced every 6 cycles (1 minute)."""
336
+ try:
337
+ self._repeat_cycles_to_next_sync -= 1
338
+ if self._repeat_cycles_to_next_sync <= 0:
339
+ self._repeat_cycles_to_next_sync = self._regular_status_sync_cycles
340
+ await self._sync_status_with_loop()
341
+ await self._update_model_if_required()
342
+ except Exception:
343
+ self.log.exception("error during '_check_for_update'")
411
344
 
412
- if not self.sio_client.connected:
413
- self.log.info('Status sync failed: not connected')
414
- raise Exception('Status sync failed: not connected')
345
+ async def _sync_status_with_loop(self) -> None:
346
+ """Sync status of the detector with the Learning Loop."""
415
347
 
416
348
  if self.detector_logic.model_info is not None:
417
349
  current_model = self.detector_logic.model_info.version
@@ -420,8 +352,8 @@ class DetectorNode(Node):
420
352
 
421
353
  target_model_version = self.target_model.version if self.target_model else None
422
354
 
423
- status = DetectionStatus(
424
- id=self.uuid,
355
+ status = DetectorStatus(
356
+ uuid=self.uuid,
425
357
  name=self.name,
426
358
  state=self.status.state,
427
359
  errors=self.status.errors,
@@ -432,49 +364,128 @@ class DetectorNode(Node):
432
364
  model_format=self.detector_logic.model_format,
433
365
  )
434
366
 
435
- self.log_status_on_change(status.state or 'None', status)
367
+ self.log_status_on_change(status.state, status)
368
+
369
+ try:
370
+ response = await self.loop_communicator.post(
371
+ f'/{self.organization}/projects/{self.project}/detectors', json=jsonable_encoder(asdict(status)))
372
+ except Exception:
373
+ self.log.warning('Exception while trying to sync status with loop')
374
+
375
+ if response.status_code != 200:
376
+ self.log.warning('Status update failed: %s', str(response))
436
377
 
437
- # NOTE: sending organization and project is no longer required!
378
+ async def _update_model_if_required(self) -> None:
379
+ """Check if a new model is available and update if necessary.
380
+ The Learning Loop will respond with the model info of the deployment target.
381
+ If version_control is set to FollowLoop or the chosen target model is not used,
382
+ the detector will update the target_model."""
438
383
  try:
439
- response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
440
- except TimeoutError:
441
- self.socket_connection_broken = True
442
- self.log.exception('TimeoutError for sending status update (will try to reconnect):')
443
- raise Exception('Status update failed due to timeout') from None
444
-
445
- if not response:
446
- self.socket_connection_broken = True
447
- self.log.error('Status update failed (will try to reconnect): %s', response)
448
- raise Exception('Status update failed: Did not receive a response from the learning loop')
449
-
450
- socket_response = from_dict(data_class=SocketResponse, data=response)
451
- if not socket_response.success:
452
- self.socket_connection_broken = True
453
- self.log.error('Status update failed (will try to reconnect): %s', response)
454
- raise Exception(f'Status update failed. Response from learning loop: {response}')
455
-
456
- assert socket_response.payload is not None
457
-
458
- deployment_target_model_id = socket_response.payload['target_model_id']
459
- deployment_target_model_version = socket_response.payload['target_model_version']
384
+ if self.operation_mode != OperationMode.Idle:
385
+ self.log.debug('not checking for updates; operation mode is %s', self.operation_mode)
386
+ return
387
+
388
+ await self._check_for_new_deployment_target()
389
+
390
+ self.status.reset_error('update_model')
391
+ if self.target_model is None:
392
+ self.log.debug('not running any updates; target model is None')
393
+ return
394
+
395
+ current_version = self.detector_logic.model_info.version \
396
+ if self.detector_logic.model_info is not None else None
397
+
398
+ if current_version != self.target_model.version:
399
+ self.log.info('Updating model from %s to %s',
400
+ current_version or "-", self.target_model.version)
401
+ await self._update_model(self.target_model)
402
+
403
+ except Exception as e:
404
+ self.log.exception('check_for_update failed')
405
+ msg = e.cause if isinstance(e, DownloadError) else str(e)
406
+ self.status.set_error('update_model', f'Could not update model: {msg}')
407
+ await self._sync_status_with_loop()
408
+
409
+ async def _check_for_new_deployment_target(self) -> None:
410
+ """Ask the learning loop for the current deployment target and update self.loop_deployment_target.
411
+ If version_control is set to FollowLoop, also update target_model."""
412
+ try:
413
+ response = await self.loop_communicator.get(
414
+ f'/{self.organization}/projects/{self.project}/deployment/target')
415
+ except Exception:
416
+ self.log.warning('Exception while trying to check for new deployment target')
417
+ return
418
+
419
+ if response.status_code != 200:
420
+ self.log.warning('Failed to check for new deployment target: %s', str(response))
421
+ return
422
+
423
+ response_data = response.json()
424
+
425
+ deployment_target_uuid = response_data['model_uuid']
426
+ deployment_target_version = response_data['version']
460
427
  self.loop_deployment_target = ModelInformation(organization=self.organization, project=self.project,
461
428
  host="", categories=[],
462
- id=deployment_target_model_id,
463
- version=deployment_target_model_version)
429
+ id=deployment_target_uuid,
430
+ version=deployment_target_version)
464
431
 
465
432
  if (self.version_control == VersionMode.FollowLoop and
466
433
  self.target_model != self.loop_deployment_target):
467
- old_target_model_version = self.target_model.version if self.target_model else None
434
+ previous_version = self.target_model.version if self.target_model else None
468
435
  self.target_model = self.loop_deployment_target
469
- self.log.info('After sending status. Target_model changed from %s to %s',
470
- old_target_model_version, self.target_model.version)
436
+ self.log.info('Deployment target changed from %s to %s',
437
+ previous_version, self.target_model.version)
438
+
439
+ async def _update_model(self, target_model: ModelInformation) -> None:
440
+ """Download and install the target model.
441
+ On failure, the target_model will be set to None which will trigger a retry on the next check."""
442
+
443
+ with step_into(GLOBALS.data_folder):
444
+ target_model_folder = f'models/{target_model.version}'
445
+ if os.path.exists(target_model_folder) and len(os.listdir(target_model_folder)) > 0:
446
+ self.log.info('No need to download model. %s (already exists)', target_model.version)
447
+ else:
448
+ os.makedirs(target_model_folder, exist_ok=True)
449
+ try:
450
+ await self.data_exchanger.download_model(target_model_folder,
451
+ Context(organization=self.organization,
452
+ project=self.project),
453
+ target_model.id, self.detector_logic.model_format)
454
+ self.log.info('Downloaded model %s', target_model.version)
455
+ except Exception:
456
+ self.log.exception('Could not download model %s', target_model.version)
457
+ shutil.rmtree(target_model_folder, ignore_errors=True)
458
+ self.target_model = None
459
+ return
460
+
461
+ model_symlink = 'model'
462
+ try:
463
+ os.unlink(model_symlink)
464
+ os.remove(model_symlink)
465
+ except Exception:
466
+ pass
467
+ os.symlink(target_model_folder, model_symlink)
468
+ self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
469
+
470
+ try:
471
+ self.detector_logic.load_model_info_and_init_model()
472
+ except NodeNeedsRestartError:
473
+ self.log.error('Node needs restart')
474
+ sys.exit(0)
475
+ except Exception:
476
+ self.log.exception('Could not load model, will retry download on next check')
477
+ shutil.rmtree(target_model_folder, ignore_errors=True)
478
+ self.target_model = None
479
+ return
480
+
481
+ await self._sync_status_with_loop()
482
+ # self.reload(reason='new model installed')
483
+
484
+ # ================================== API Implementations ==================================
471
485
 
472
486
  async def set_operation_mode(self, mode: OperationMode):
473
487
  self.operation_mode = mode
474
- try:
475
- await self.sync_status_with_learning_loop()
476
- except Exception as e:
477
- self.log.warning('Operation mode set to %s, but sync failed: %s', mode, e)
488
+ await self._sync_status_with_loop()
478
489
 
479
490
  def reload(self, reason: str):
480
491
  """provide a cause for the reload"""
@@ -189,7 +189,7 @@ class Outbox():
189
189
 
190
190
  async def _continuous_upload(self) -> None:
191
191
  self.log.info('continuous upload started')
192
- assert self.shutdown_event is not None
192
+ assert self.shutdown_event is not None, 'shutdown_event is None'
193
193
  while not self.shutdown_event.is_set():
194
194
  await self.upload()
195
195
  await asyncio.sleep(self.UPLOAD_INTERVAL_S)
@@ -287,7 +287,7 @@ class Outbox():
287
287
  return True
288
288
 
289
289
  try:
290
- assert self.shutdown_event is not None
290
+ assert self.shutdown_event is not None, 'shutdown_event is None'
291
291
  self.shutdown_event.set()
292
292
  await asyncio.wait_for(self.upload_task, timeout=self.UPLOAD_TIMEOUT_S + 1)
293
293
  except asyncio.TimeoutError:
@@ -8,7 +8,8 @@ from httpx import Cookies, Timeout
8
8
 
9
9
  from .helpers import environment_reader
10
10
 
11
- logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger('loop_communication')
12
+ logging.getLogger("httpx").setLevel(logging.WARNING)
12
13
 
13
14
  SLEEP_TIME_ON_429 = 5
14
15
  MAX_RETRIES_ON_429 = 20
@@ -37,9 +38,9 @@ class LoopCommunicator():
37
38
  host: str = environment_reader.host(default='learning-loop.ai')
38
39
  self.ssl_cert_path = environment_reader.ssl_certificate_path()
39
40
  if self.ssl_cert_path:
40
- logging.info('Using SSL certificate at %s', self.ssl_cert_path)
41
+ logger.info('Using SSL certificate at %s', self.ssl_cert_path)
41
42
  else:
42
- logging.info('No SSL certificate path set')
43
+ logger.info('No SSL certificate path set')
43
44
  self.host: str = host
44
45
  self.username: str = environment_reader.username()
45
46
  self.password: str = environment_reader.password()
@@ -52,7 +53,7 @@ class LoopCommunicator():
52
53
  else:
53
54
  self.async_client = httpx.AsyncClient(base_url=self.base_url, timeout=Timeout(60.0))
54
55
 
55
- logging.info('Loop interface initialized with base_url: %s / user: %s', self.base_url, self.username)
56
+ logger.info('Loop interface initialized with base_url: %s / user: %s', self.base_url, self.username)
56
57
 
57
58
  def websocket_url(self) -> str:
58
59
  return f'ws{"s" if "learning-loop.ai" in self.host else ""}://' + self.host
@@ -65,7 +66,7 @@ class LoopCommunicator():
65
66
  self.async_client.cookies.clear()
66
67
  response = await self.async_client.post('/api/login', data={'username': self.username, 'password': self.password})
67
68
  if response.status_code != 200:
68
- logging.info('Login failed with response: %s', response)
69
+ logger.info('Login failed with response: %s', response)
69
70
  raise LoopCommunicationException('Login failed with response: ' + str(response))
70
71
  self.async_client.cookies.update(response.cookies)
71
72
 
@@ -74,7 +75,7 @@ class LoopCommunicator():
74
75
 
75
76
  response = await self.async_client.post('/api/logout')
76
77
  if response.status_code != 200:
77
- logging.info('Logout failed with response: %s', response)
78
+ logger.info('Logout failed with response: %s', response)
78
79
  raise LoopCommunicationException('Logout failed with response: ' + str(response))
79
80
  self.async_client.cookies.clear()
80
81
 
@@ -90,12 +91,12 @@ class LoopCommunicator():
90
91
  start_time = time.time()
91
92
  while True:
92
93
  try:
93
- logging.info('Checking if backend is ready')
94
+ logger.info('Checking if backend is ready')
94
95
  response = await self.get('/status', requires_login=False)
95
96
  if response.status_code == 200:
96
97
  return True
97
98
  except Exception:
98
- logging.info('backend not ready yet.')
99
+ logger.info('backend not ready yet.')
99
100
  if timeout is not None and time.time() + 10 - start_time > timeout:
100
101
  raise TimeoutError('Backend not ready within timeout')
101
102
  await asyncio.sleep(10)