juniper-canopy 0.3.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.
- backend/__init__.py +59 -0
- backend/cascor_service_adapter.py +306 -0
- backend/cassandra_client.py +499 -0
- backend/data_adapter.py +534 -0
- backend/demo_backend.py +253 -0
- backend/protocol.py +140 -0
- backend/redis_client.py +536 -0
- backend/service_backend.py +135 -0
- backend/statistics.py +241 -0
- backend/training_monitor.py +623 -0
- backend/training_state_machine.py +329 -0
- communication/__init__.py +5 -0
- communication/websocket_manager.py +749 -0
- frontend/__init__.py +9 -0
- frontend/base_component.py +101 -0
- frontend/callback_context.py +153 -0
- frontend/components/__init__.py +3 -0
- frontend/components/about_panel.py +351 -0
- frontend/components/cassandra_panel.py +483 -0
- frontend/components/dataset_plotter.py +485 -0
- frontend/components/decision_boundary.py +444 -0
- frontend/components/hdf5_snapshots_panel.py +1080 -0
- frontend/components/metrics_panel.py +1823 -0
- frontend/components/network_visualizer.py +1433 -0
- frontend/components/redis_panel.py +534 -0
- frontend/components/training_metrics.py +73 -0
- frontend/dashboard_manager.py +1539 -0
- juniper_canopy/__init__.py +4 -0
- juniper_canopy-0.3.0.dist-info/METADATA +277 -0
- juniper_canopy-0.3.0.dist-info/RECORD +35 -0
- juniper_canopy-0.3.0.dist-info/WHEEL +5 -0
- juniper_canopy-0.3.0.dist-info/licenses/LICENSE +9 -0
- juniper_canopy-0.3.0.dist-info/top_level.txt +5 -0
- logger/__init__.py +0 -0
- logger/logger.py +622 -0
backend/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend Integration Package
|
|
3
|
+
|
|
4
|
+
Interfaces with the CasCor neural network backend for monitoring and data collection.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- BackendProtocol: Unified interface for all backend implementations
|
|
8
|
+
- DemoBackend: Adapter wrapping DemoMode for development/testing
|
|
9
|
+
- ServiceBackend: Adapter wrapping CascorServiceAdapter for production (lazy import)
|
|
10
|
+
- create_backend(): Factory function selecting the appropriate backend
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from backend.demo_backend import DemoBackend
|
|
17
|
+
from backend.protocol import BackendProtocol
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BackendProtocol",
|
|
21
|
+
"DemoBackend",
|
|
22
|
+
"create_backend",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("juniper_canopy.backend")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_backend() -> BackendProtocol:
|
|
29
|
+
"""
|
|
30
|
+
Factory: create the appropriate backend based on environment.
|
|
31
|
+
|
|
32
|
+
Selection logic:
|
|
33
|
+
1. CASCOR_DEMO_MODE=1/true/yes -> DemoBackend (explicit demo)
|
|
34
|
+
2. CASCOR_SERVICE_URL set -> ServiceBackend (real CasCor)
|
|
35
|
+
3. Otherwise -> DemoBackend (fallback)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A BackendProtocol-conforming backend instance.
|
|
39
|
+
"""
|
|
40
|
+
from demo_mode import get_demo_mode
|
|
41
|
+
|
|
42
|
+
force_demo = os.getenv("CASCOR_DEMO_MODE", "0").lower() in ("1", "true", "yes")
|
|
43
|
+
service_url = os.getenv("CASCOR_SERVICE_URL")
|
|
44
|
+
|
|
45
|
+
if force_demo:
|
|
46
|
+
logger.info("Demo mode explicitly enabled via CASCOR_DEMO_MODE")
|
|
47
|
+
return DemoBackend(get_demo_mode(update_interval=1.0))
|
|
48
|
+
|
|
49
|
+
if service_url:
|
|
50
|
+
from backend.cascor_service_adapter import CascorServiceAdapter
|
|
51
|
+
from backend.service_backend import ServiceBackend
|
|
52
|
+
|
|
53
|
+
api_key = os.getenv("JUNIPER_DATA_API_KEY")
|
|
54
|
+
logger.info(f"Service mode: connecting to CasCor at {service_url}")
|
|
55
|
+
adapter = CascorServiceAdapter(service_url=service_url, api_key=api_key)
|
|
56
|
+
return ServiceBackend(adapter)
|
|
57
|
+
|
|
58
|
+
logger.info("No CASCOR_SERVICE_URL set — falling back to demo mode")
|
|
59
|
+
return DemoBackend(get_demo_mode(update_interval=1.0))
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#####################################################################################################################################################################################################
|
|
3
|
+
# Project: Juniper
|
|
4
|
+
# Sub-Project: JuniperCanopy
|
|
5
|
+
# Application: juniper_canopy
|
|
6
|
+
# Purpose: Service adapter wrapping juniper-cascor-client for REST/WebSocket communication with CasCor service
|
|
7
|
+
#
|
|
8
|
+
# Author: Paul Calnon
|
|
9
|
+
# Version: 0.1.0
|
|
10
|
+
# File Name: cascor_service_adapter.py
|
|
11
|
+
# File Path: Juniper/juniper-canopy/src/backend/
|
|
12
|
+
#
|
|
13
|
+
# Date Created: 2026-02-21
|
|
14
|
+
# Last Modified: 2026-02-21
|
|
15
|
+
#
|
|
16
|
+
# License: MIT License
|
|
17
|
+
# Copyright: Copyright (c) 2024,2025,2026 Paul Calnon
|
|
18
|
+
#
|
|
19
|
+
# Description:
|
|
20
|
+
# CascorServiceAdapter wraps the juniper-cascor-client package to provide
|
|
21
|
+
# a backward-compatible interface matching CascorIntegration's public API.
|
|
22
|
+
# This enables Canopy to communicate with CasCor as an independent service
|
|
23
|
+
# over REST/WebSocket instead of in-process sys.path injection.
|
|
24
|
+
#
|
|
25
|
+
#####################################################################################################################################################################################################
|
|
26
|
+
# Notes:
|
|
27
|
+
# Phase 4 of the Juniper polyrepo migration — Decouple Canopy from CasCor.
|
|
28
|
+
# All methods match the CascorIntegration interface used by main.py.
|
|
29
|
+
#
|
|
30
|
+
#####################################################################################################################################################################################################
|
|
31
|
+
# References:
|
|
32
|
+
# - juniper-cascor-client v0.1.0 API
|
|
33
|
+
# - notes/DECOUPLE_CANOPY_FROM_CASCOR_PLAN.md
|
|
34
|
+
#
|
|
35
|
+
#####################################################################################################################################################################################################
|
|
36
|
+
|
|
37
|
+
import asyncio
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Any, Callable, Dict, Optional, Tuple, Union
|
|
40
|
+
|
|
41
|
+
from juniper_cascor_client import CascorTrainingStream, JuniperCascorClient
|
|
42
|
+
from juniper_cascor_client.exceptions import JuniperCascorClientError
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("juniper_canopy.backend.cascor_service_adapter")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _ServiceTrainingMonitor:
|
|
48
|
+
"""
|
|
49
|
+
Lightweight training monitor that delegates to the CasCor service via REST.
|
|
50
|
+
|
|
51
|
+
Satisfies the subset of TrainingMonitor's interface used by main.py:
|
|
52
|
+
- .is_training (property)
|
|
53
|
+
- .get_current_metrics()
|
|
54
|
+
- .get_recent_metrics(count)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, client: JuniperCascorClient):
|
|
58
|
+
self._client = client
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_training(self) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
status = self._client.get_training_status()
|
|
64
|
+
return status.get("is_training", False)
|
|
65
|
+
except JuniperCascorClientError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def get_current_metrics(self) -> Dict[str, Any]:
|
|
69
|
+
try:
|
|
70
|
+
return self._client.get_metrics()
|
|
71
|
+
except JuniperCascorClientError:
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
def get_recent_metrics(self, count: int = 100) -> list:
|
|
75
|
+
try:
|
|
76
|
+
result = self._client.get_metrics_history(count=count)
|
|
77
|
+
return result.get("history", []) if isinstance(result, dict) else result
|
|
78
|
+
except JuniperCascorClientError:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _NetworkSentinel:
|
|
83
|
+
"""Truthy sentinel representing a remote network exists."""
|
|
84
|
+
|
|
85
|
+
def __bool__(self):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
return "<RemoteNetwork>"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CascorServiceAdapter:
|
|
93
|
+
"""
|
|
94
|
+
Adapter wrapping juniper-cascor-client to provide a CascorIntegration-compatible
|
|
95
|
+
interface for main.py. Communicates with CasCor over REST/WebSocket.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
_is_service_adapter = True
|
|
99
|
+
|
|
100
|
+
def __init__(self, service_url: str = "http://localhost:8200", api_key: Optional[str] = None):
|
|
101
|
+
self._service_url = service_url
|
|
102
|
+
self._api_key = api_key
|
|
103
|
+
self._client = JuniperCascorClient(base_url=service_url, api_key=api_key)
|
|
104
|
+
self.training_monitor = _ServiceTrainingMonitor(self._client)
|
|
105
|
+
self._training_stream: Optional[CascorTrainingStream] = None
|
|
106
|
+
self._relay_task: Optional[asyncio.Task] = None
|
|
107
|
+
|
|
108
|
+
# Derive WebSocket URL from HTTP URL
|
|
109
|
+
ws_url = service_url.replace("http://", "ws://").replace("https://", "wss://")
|
|
110
|
+
self._ws_url = ws_url
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Connection lifecycle (async)
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async def connect(self) -> bool:
|
|
117
|
+
"""Connect to the CasCor service and verify it is ready."""
|
|
118
|
+
try:
|
|
119
|
+
return self._client.is_ready()
|
|
120
|
+
except JuniperCascorClientError:
|
|
121
|
+
logger.error(f"Failed to connect to CasCor service at {self._service_url}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
async def start_metrics_relay(self) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Open a WebSocket training stream and relay messages to Canopy's
|
|
127
|
+
websocket_manager for broadcast to dashboard clients.
|
|
128
|
+
"""
|
|
129
|
+
from communication.websocket_manager import websocket_manager
|
|
130
|
+
|
|
131
|
+
self._training_stream = CascorTrainingStream(base_url=self._ws_url, api_key=self._api_key)
|
|
132
|
+
|
|
133
|
+
async def _relay_loop():
|
|
134
|
+
try:
|
|
135
|
+
await self._training_stream.connect()
|
|
136
|
+
async for message in self._training_stream.stream():
|
|
137
|
+
msg_type = message.get("type", "")
|
|
138
|
+
data = message.get("data", message)
|
|
139
|
+
await websocket_manager.broadcast({"type": msg_type, "data": data})
|
|
140
|
+
except asyncio.CancelledError:
|
|
141
|
+
pass
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(f"Metrics relay error: {e}")
|
|
144
|
+
finally:
|
|
145
|
+
await self._training_stream.disconnect()
|
|
146
|
+
|
|
147
|
+
self._relay_task = asyncio.create_task(_relay_loop())
|
|
148
|
+
logger.info("Metrics relay started")
|
|
149
|
+
|
|
150
|
+
async def stop_metrics_relay(self) -> None:
|
|
151
|
+
"""Cancel the WebSocket relay task."""
|
|
152
|
+
if self._relay_task and not self._relay_task.done():
|
|
153
|
+
self._relay_task.cancel()
|
|
154
|
+
try:
|
|
155
|
+
await self._relay_task
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
pass
|
|
158
|
+
self._relay_task = None
|
|
159
|
+
logger.info("Metrics relay stopped")
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Network property (lines 491, 1803 in main.py)
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def network(self) -> Optional[_NetworkSentinel]:
|
|
167
|
+
"""Return a truthy sentinel if the service has a network, else None."""
|
|
168
|
+
try:
|
|
169
|
+
result = self._client.get_network()
|
|
170
|
+
if result and not result.get("error"):
|
|
171
|
+
return _NetworkSentinel()
|
|
172
|
+
except JuniperCascorClientError:
|
|
173
|
+
pass
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# _training_stop_requested (line 1920 in main.py)
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def _training_stop_requested(self) -> bool:
|
|
182
|
+
"""Service manages stop requests internally."""
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Network creation & management
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def create_network(self, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
190
|
+
try:
|
|
191
|
+
return self._client.create_network(**(config or {}))
|
|
192
|
+
except JuniperCascorClientError as e:
|
|
193
|
+
logger.error(f"Failed to create network: {e}")
|
|
194
|
+
return {"error": str(e)}
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
# Training control
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def start_training_background(self, *args, **kwargs) -> bool:
|
|
201
|
+
try:
|
|
202
|
+
self._client.start_training(**kwargs)
|
|
203
|
+
return True
|
|
204
|
+
except JuniperCascorClientError as e:
|
|
205
|
+
logger.error(f"Failed to start training: {e}")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def is_training_in_progress(self) -> bool:
|
|
209
|
+
try:
|
|
210
|
+
status = self._client.get_training_status()
|
|
211
|
+
return status.get("is_training", False)
|
|
212
|
+
except JuniperCascorClientError:
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def request_training_stop(self) -> bool:
|
|
216
|
+
try:
|
|
217
|
+
self._client.stop_training()
|
|
218
|
+
return True
|
|
219
|
+
except JuniperCascorClientError as e:
|
|
220
|
+
logger.error(f"Failed to stop training: {e}")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Status & metrics
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def get_training_status(self) -> Dict[str, Any]:
|
|
228
|
+
try:
|
|
229
|
+
return self._client.get_training_status()
|
|
230
|
+
except JuniperCascorClientError as e:
|
|
231
|
+
logger.error(f"Failed to get training status: {e}")
|
|
232
|
+
return {"is_training": False, "error": str(e)}
|
|
233
|
+
|
|
234
|
+
def get_network_data(self) -> Dict[str, Any]:
|
|
235
|
+
try:
|
|
236
|
+
return self._client.get_statistics()
|
|
237
|
+
except JuniperCascorClientError as e:
|
|
238
|
+
logger.error(f"Failed to get network data: {e}")
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
def extract_network_topology(self) -> Optional[Dict[str, Any]]:
|
|
242
|
+
try:
|
|
243
|
+
return self._client.get_topology()
|
|
244
|
+
except JuniperCascorClientError:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def get_network_topology(self) -> Optional[Dict[str, Any]]:
|
|
248
|
+
return self.extract_network_topology()
|
|
249
|
+
|
|
250
|
+
def get_dataset_info(self, x=None, y=None) -> Optional[Dict[str, Any]]:
|
|
251
|
+
try:
|
|
252
|
+
return self._client.get_dataset()
|
|
253
|
+
except JuniperCascorClientError:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def get_prediction_function(self) -> Optional[Callable]:
|
|
257
|
+
"""Not available over REST — returns None."""
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# Monitoring no-ops (hooks are in-process CascorIntegration only)
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def install_monitoring_hooks(self) -> bool:
|
|
265
|
+
return True
|
|
266
|
+
|
|
267
|
+
def start_monitoring_thread(self, interval: float = 1.0) -> None:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
def stop_monitoring(self) -> None:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def restore_original_methods(self) -> None:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
def create_monitoring_callback(self, event_type: str, callback: Callable) -> None:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
# Remote worker no-ops (workers managed by the CasCor service)
|
|
281
|
+
# ------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def get_remote_worker_status(self) -> Dict[str, Any]:
|
|
284
|
+
return {"available": False, "connected": False, "workers_active": False, "error": "Managed by CasCor service"}
|
|
285
|
+
|
|
286
|
+
def connect_remote_workers(self, address: Tuple[str, int], authkey: Union[str, bytes]) -> bool:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def start_remote_workers(self, num_workers: int = 1) -> bool:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def stop_remote_workers(self, timeout: int = 10) -> bool:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def disconnect_remote_workers(self) -> bool:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# ------------------------------------------------------------------
|
|
299
|
+
# Shutdown
|
|
300
|
+
# ------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
def shutdown(self) -> None:
|
|
303
|
+
try:
|
|
304
|
+
self._client.close()
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Error during shutdown: {e}")
|