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 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}")