lanscape 1.3.8a1__py3-none-any.whl → 2.4.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lanscape might be problematic. Click here for more details.

Files changed (58) hide show
  1. lanscape/__init__.py +8 -4
  2. lanscape/{libraries → core}/app_scope.py +21 -3
  3. lanscape/core/decorators.py +231 -0
  4. lanscape/{libraries → core}/device_alive.py +83 -16
  5. lanscape/{libraries → core}/ip_parser.py +2 -26
  6. lanscape/{libraries → core}/net_tools.py +209 -66
  7. lanscape/{libraries → core}/runtime_args.py +6 -0
  8. lanscape/{libraries → core}/scan_config.py +103 -5
  9. lanscape/core/service_scan.py +222 -0
  10. lanscape/{libraries → core}/subnet_scan.py +30 -14
  11. lanscape/{libraries → core}/version_manager.py +15 -17
  12. lanscape/resources/ports/test_port_list_scan.json +4 -0
  13. lanscape/resources/services/definitions.jsonc +576 -400
  14. lanscape/ui/app.py +17 -5
  15. lanscape/ui/blueprints/__init__.py +1 -1
  16. lanscape/ui/blueprints/api/port.py +15 -1
  17. lanscape/ui/blueprints/api/scan.py +1 -1
  18. lanscape/ui/blueprints/api/tools.py +4 -4
  19. lanscape/ui/blueprints/web/routes.py +29 -2
  20. lanscape/ui/main.py +46 -19
  21. lanscape/ui/shutdown_handler.py +2 -2
  22. lanscape/ui/static/css/style.css +186 -20
  23. lanscape/ui/static/js/core.js +14 -0
  24. lanscape/ui/static/js/main.js +30 -2
  25. lanscape/ui/static/js/quietReload.js +3 -0
  26. lanscape/ui/static/js/scan-config.js +56 -6
  27. lanscape/ui/templates/base.html +6 -8
  28. lanscape/ui/templates/core/head.html +1 -1
  29. lanscape/ui/templates/info.html +20 -5
  30. lanscape/ui/templates/main.html +33 -36
  31. lanscape/ui/templates/scan/config.html +214 -176
  32. lanscape/ui/templates/scan/device-detail.html +111 -0
  33. lanscape/ui/templates/scan/ip-table-row.html +17 -83
  34. lanscape/ui/templates/scan/ip-table.html +5 -5
  35. lanscape/ui/ws/__init__.py +31 -0
  36. lanscape/ui/ws/delta.py +170 -0
  37. lanscape/ui/ws/handlers/__init__.py +20 -0
  38. lanscape/ui/ws/handlers/base.py +145 -0
  39. lanscape/ui/ws/handlers/port.py +184 -0
  40. lanscape/ui/ws/handlers/scan.py +352 -0
  41. lanscape/ui/ws/handlers/tools.py +145 -0
  42. lanscape/ui/ws/protocol.py +86 -0
  43. lanscape/ui/ws/server.py +375 -0
  44. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/METADATA +18 -3
  45. lanscape-2.4.0a2.dist-info/RECORD +85 -0
  46. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/WHEEL +1 -1
  47. lanscape-2.4.0a2.dist-info/entry_points.txt +2 -0
  48. lanscape/libraries/decorators.py +0 -170
  49. lanscape/libraries/service_scan.py +0 -50
  50. lanscape/libraries/web_browser.py +0 -210
  51. lanscape-1.3.8a1.dist-info/RECORD +0 -74
  52. /lanscape/{libraries → core}/__init__.py +0 -0
  53. /lanscape/{libraries → core}/errors.py +0 -0
  54. /lanscape/{libraries → core}/logger.py +0 -0
  55. /lanscape/{libraries → core}/mac_lookup.py +0 -0
  56. /lanscape/{libraries → core}/port_manager.py +0 -0
  57. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/licenses/LICENSE +0 -0
  58. {lanscape-1.3.8a1.dist-info → lanscape-2.4.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,375 @@
1
+ """
2
+ WebSocket server for LANscape.
3
+
4
+ Provides an async WebSocket server that can run independently of the Flask UI.
5
+ Handles client connections, message routing, and real-time scan updates.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import uuid
12
+ from typing import Optional
13
+
14
+ import websockets
15
+ from websockets.server import WebSocketServerProtocol
16
+
17
+ from lanscape.ui.ws.protocol import (
18
+ WSRequest,
19
+ WSResponse,
20
+ WSError,
21
+ WSEvent
22
+ )
23
+ from lanscape.ui.ws.handlers import (
24
+ ScanHandler,
25
+ PortHandler,
26
+ ToolsHandler
27
+ )
28
+
29
+ class WebSocketServer:
30
+ """
31
+ Async WebSocket server for LANscape.
32
+
33
+ Provides a standalone WebSocket interface to all LANscape functionality.
34
+ Supports real-time scan updates via subscriptions.
35
+ """
36
+
37
+ DEFAULT_HOST = 'localhost'
38
+ DEFAULT_PORT = 8766
39
+
40
+ def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
41
+ """
42
+ Initialize the WebSocket server.
43
+
44
+ Args:
45
+ host: Host to bind to (default: 0.0.0.0)
46
+ port: Port to listen on (default: 8766)
47
+ """
48
+ self.host = host
49
+ self.port = port
50
+ self.log = logging.getLogger('WebSocketServer')
51
+
52
+ # Initialize handlers
53
+ self._scan_handler = ScanHandler()
54
+ self._port_handler = PortHandler()
55
+ self._tools_handler = ToolsHandler()
56
+
57
+ self._handlers = [
58
+ self._scan_handler,
59
+ self._port_handler,
60
+ self._tools_handler
61
+ ]
62
+
63
+ # Active connections
64
+ self._clients: dict[str, WebSocketServerProtocol] = {}
65
+
66
+ # Track scans that were running (to detect completion)
67
+ self._previously_running_scans: set[str] = set()
68
+
69
+ # Server instance
70
+ self._server = None
71
+ self._running = False
72
+
73
+ # Background tasks
74
+ self._update_task: Optional[asyncio.Task] = None
75
+
76
+ def get_actions(self) -> list[str]:
77
+ """
78
+ Get all supported actions.
79
+
80
+ Returns:
81
+ List of all action names supported by all handlers
82
+ """
83
+ actions = []
84
+ for handler in self._handlers:
85
+ actions.extend(handler.get_actions())
86
+ return actions
87
+
88
+ async def start(self) -> None:
89
+ """Start the WebSocket server."""
90
+ self.log.info(f"Starting WebSocket server on ws://{self.host}:{self.port}")
91
+
92
+ self._running = True
93
+
94
+ # Minimal WebSocket server configuration - let the library handle everything
95
+ self._server = await websockets.serve(
96
+ self._handle_connection,
97
+ self.host,
98
+ self.port
99
+ )
100
+
101
+ # Start the background update task
102
+ self._update_task = asyncio.create_task(self._broadcast_scan_updates())
103
+
104
+ self.log.info("WebSocket server started")
105
+
106
+ async def stop(self) -> None:
107
+ """Stop the WebSocket server."""
108
+ self.log.info("Stopping WebSocket server...")
109
+ self._running = False
110
+
111
+ if self._update_task:
112
+ self._update_task.cancel()
113
+ try:
114
+ await self._update_task
115
+ except asyncio.CancelledError:
116
+ pass
117
+
118
+ if self._server:
119
+ self._server.close()
120
+ await self._server.wait_closed()
121
+
122
+ # Close all client connections
123
+ for client_id, ws in list(self._clients.items()):
124
+ try:
125
+ await ws.close()
126
+ except Exception as e:
127
+ self.log.debug(f"Error closing client {client_id}: {e}")
128
+
129
+ self._clients.clear()
130
+ self.log.info("WebSocket server stopped")
131
+
132
+ async def serve_forever(self) -> None:
133
+ """Run the server until stopped."""
134
+ await self.start()
135
+ try:
136
+ await self._server.wait_closed()
137
+ except asyncio.CancelledError:
138
+ await self.stop()
139
+
140
+ async def _handle_connection(
141
+ self,
142
+ websocket: WebSocketServerProtocol
143
+ ) -> None:
144
+ """
145
+ Handle a new WebSocket connection.
146
+
147
+ Args:
148
+ websocket: The WebSocket connection
149
+ """
150
+ client_id = str(uuid.uuid4())
151
+ self._clients[client_id] = websocket
152
+ self.log.info(f"Client connected: {client_id}")
153
+
154
+ # Send welcome message with client_id
155
+ await self._send_event(
156
+ websocket,
157
+ 'connection.established',
158
+ {'client_id': client_id, 'actions': self.get_actions()}
159
+ )
160
+
161
+ try:
162
+ async for message in websocket:
163
+ await self._handle_message(client_id, websocket, message)
164
+ except websockets.ConnectionClosed:
165
+ self.log.info(f"Client disconnected: {client_id}")
166
+ except Exception as e:
167
+ self.log.error(f"Error handling client {client_id}: {e}")
168
+ finally:
169
+ self._cleanup_client(client_id)
170
+
171
+ async def _handle_message(
172
+ self,
173
+ client_id: str,
174
+ websocket: WebSocketServerProtocol,
175
+ message: str
176
+ ) -> None:
177
+ """
178
+ Handle an incoming WebSocket message.
179
+
180
+ Args:
181
+ client_id: The client identifier
182
+ websocket: The WebSocket connection
183
+ message: The raw message string
184
+ """
185
+ try:
186
+ data = json.loads(message)
187
+ request = WSRequest.model_validate(data)
188
+ except json.JSONDecodeError as e:
189
+ error = WSError(error=f"Invalid JSON: {e}")
190
+ await self._send(websocket, error)
191
+ return
192
+ except Exception as e:
193
+ error = WSError(error=f"Invalid request format: {e}")
194
+ await self._send(websocket, error)
195
+ return
196
+
197
+ self.log.debug(f"[{client_id}] Request: {request.action}")
198
+
199
+ # Find the appropriate handler
200
+ response = None
201
+ for handler in self._handlers:
202
+ if handler.can_handle(request.action):
203
+ # Create a send_event callback for this client
204
+ async def send_event(event: str, data: dict) -> None:
205
+ await self._send_event(websocket, event, data)
206
+
207
+ response = await handler.handle(request, send_event)
208
+ break
209
+
210
+ if response is None:
211
+ response = WSError(
212
+ id=request.id,
213
+ action=request.action,
214
+ error=f"Unknown action: {request.action}. "
215
+ f"Available actions: {self.get_actions()}"
216
+ )
217
+
218
+ await self._send(websocket, response)
219
+
220
+ async def _send(
221
+ self,
222
+ websocket: WebSocketServerProtocol,
223
+ message: WSResponse | WSError | WSEvent
224
+ ) -> None:
225
+ """
226
+ Send a message to a client.
227
+
228
+ Args:
229
+ websocket: The WebSocket connection
230
+ message: The message to send
231
+ """
232
+ try:
233
+ await websocket.send(message.model_dump_json())
234
+ except websockets.ConnectionClosed:
235
+ pass
236
+ except Exception as e:
237
+ self.log.error(f"Error sending message: {e}")
238
+
239
+ async def _send_event(
240
+ self,
241
+ websocket: WebSocketServerProtocol,
242
+ event: str,
243
+ data: dict
244
+ ) -> None:
245
+ """
246
+ Send an event to a client.
247
+
248
+ Args:
249
+ websocket: The WebSocket connection
250
+ event: The event name
251
+ data: The event data
252
+ """
253
+ message = WSEvent(event=event, data=data)
254
+ await self._send(websocket, message)
255
+
256
+ async def _broadcast_scan_updates(self) -> None:
257
+ """
258
+ Background task to broadcast scan updates to subscribed clients.
259
+
260
+ Sends delta updates every 500ms for active scans.
261
+ """
262
+ while self._running:
263
+ try:
264
+ await asyncio.sleep(0.5)
265
+ await self._send_updates_for_active_scans()
266
+ except asyncio.CancelledError:
267
+ break
268
+ except Exception as e:
269
+ self.log.error(f"Error in broadcast loop: {e}")
270
+
271
+ async def _send_updates_for_active_scans(self) -> None:
272
+ """Send delta updates for all active scans to subscribed clients."""
273
+ # pylint: disable=protected-access
274
+ currently_running = set()
275
+
276
+ for scan in self._scan_handler._scan_manager.scans:
277
+ if scan.running:
278
+ currently_running.add(scan.uid)
279
+ await self._send_scan_update_to_subscribers(scan)
280
+ elif scan.uid in self._previously_running_scans:
281
+ # Scan just completed - send final update with complete event
282
+ await self._send_scan_complete_to_subscribers(scan)
283
+
284
+ # Update tracking set
285
+ self._previously_running_scans = currently_running
286
+
287
+ async def _send_scan_complete_to_subscribers(self, scan) -> None:
288
+ """Send scan complete event to all subscribed clients."""
289
+ subscribed_clients = self._scan_handler.get_subscriptions(scan.uid)
290
+
291
+ for client_id in subscribed_clients:
292
+ websocket = self._clients.get(client_id)
293
+ if websocket is None:
294
+ continue
295
+
296
+ try:
297
+ # Send final delta with all remaining changes
298
+ # pylint: disable=protected-access
299
+ delta = self._scan_handler._handle_get_delta(
300
+ {'scan_id': scan.uid, 'client_id': client_id},
301
+ None
302
+ )
303
+ # Force the complete stage in metadata
304
+ if 'metadata' in delta:
305
+ delta['metadata']['running'] = False
306
+ delta['metadata']['stage'] = 'complete'
307
+
308
+ await self._send_event(websocket, 'scan.complete', delta)
309
+ except Exception as e:
310
+ self.log.debug(f"Error sending complete to {client_id}: {e}")
311
+
312
+ async def _send_scan_update_to_subscribers(self, scan) -> None:
313
+ """Send scan update to all subscribed clients."""
314
+ subscribed_clients = self._scan_handler.get_subscriptions(scan.uid)
315
+
316
+ for client_id in subscribed_clients:
317
+ websocket = self._clients.get(client_id)
318
+ if websocket is None:
319
+ continue
320
+
321
+ await self._try_send_delta_update(websocket, scan.uid, client_id)
322
+
323
+ async def _try_send_delta_update(
324
+ self,
325
+ websocket: WebSocketServerProtocol,
326
+ scan_id: str,
327
+ client_id: str
328
+ ) -> None:
329
+ """Try to send a delta update to a client."""
330
+ try:
331
+ # pylint: disable=protected-access
332
+ delta = self._scan_handler._handle_get_delta(
333
+ {'scan_id': scan_id, 'client_id': client_id},
334
+ None
335
+ )
336
+
337
+ if delta.get('has_changes'):
338
+ await self._send_event(websocket, 'scan.update', delta)
339
+ except Exception as e:
340
+ self.log.debug(f"Error sending update to {client_id}: {e}")
341
+
342
+ def _cleanup_client(self, client_id: str) -> None:
343
+ """
344
+ Clean up resources for a disconnected client.
345
+
346
+ Args:
347
+ client_id: The client identifier
348
+ """
349
+ self._clients.pop(client_id, None)
350
+ self._scan_handler.cleanup_client(client_id)
351
+ self.log.debug(f"Cleaned up client: {client_id}")
352
+
353
+
354
+ def run_server(host: str = WebSocketServer.DEFAULT_HOST,
355
+ port: int = WebSocketServer.DEFAULT_PORT) -> None:
356
+ """
357
+ Run the WebSocket server.
358
+
359
+ This is a convenience function to start the server synchronously.
360
+
361
+ Args:
362
+ host: Host to bind to
363
+ port: Port to listen on
364
+ """
365
+ server = WebSocketServer(host, port)
366
+ asyncio.run(server.serve_forever())
367
+
368
+
369
+ if __name__ == '__main__':
370
+ # Configure logging when run directly
371
+ logging.basicConfig(
372
+ level=logging.INFO,
373
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
374
+ )
375
+ run_server()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 1.3.8a1
3
+ Version: 2.4.0a2
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -8,8 +8,13 @@ Project-URL: Homepage, https://github.com/mdennis281/py-lanscape
8
8
  Project-URL: Issues, https://github.com/mdennis281/py-lanscape/issues
9
9
  Keywords: network,scanner,lan,local,python
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
11
16
  Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.8
17
+ Requires-Python: >=3.10
13
18
  Description-Content-Type: text/markdown
14
19
  License-File: LICENSE
15
20
  Requires-Dist: Flask<5.0,>=3.0
@@ -20,6 +25,16 @@ Requires-Dist: scapy<3.0,>=2.3.2
20
25
  Requires-Dist: tabulate==0.9.0
21
26
  Requires-Dist: pydantic
22
27
  Requires-Dist: icmplib
28
+ Requires-Dist: pwa-launcher>=1.1.0
29
+ Requires-Dist: websockets<14.0,>=12.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
33
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
35
+ Requires-Dist: openai>=1.0.0; extra == "dev"
36
+ Requires-Dist: pylint>=3.0; extra == "dev"
37
+ Requires-Dist: autopep8>=2.0; extra == "dev"
23
38
  Dynamic: license-file
24
39
 
25
40
  # LANscape
@@ -80,7 +95,7 @@ I use a combination of ARP, ICMP & port testing to determine if a device is onli
80
95
  Recommendations:
81
96
 
82
97
  - Adjust scan configuration
83
- - Configure ARP lookup [ARP lookup setup](./support/arp-issues.md)
98
+ - Configure ARP lookup [ARP lookup setup](./docs/arp-issues.md)
84
99
  - Create a bug
85
100
 
86
101
 
@@ -0,0 +1,85 @@
1
+ lanscape/__init__.py,sha256=ibc4hZ6Esm7fsqocLRpc2v30BVWrKpFQ-iMJisoDtL8,423
2
+ lanscape/__main__.py,sha256=PuY42yuCLAwHrOREJ6u2DgVyGX5hZKRQeoE9pajkNfM,170
3
+ lanscape/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ lanscape/core/app_scope.py,sha256=qfzX8Ed4bFdxHMGjgnLlWuLZDTCBKObermz91KbGVn0,3298
5
+ lanscape/core/decorators.py,sha256=CZbPEfnLS1OF-uejQweetadzqf0pVo736jKko4Xs-g4,7264
6
+ lanscape/core/device_alive.py,sha256=VY2dsoy6_MWUxcysZEFcsSoCtDkLMYzwqy0U_sbVWE0,9609
7
+ lanscape/core/errors.py,sha256=QTf42UzR9Zxj1t1mdwfLvZIp0c9a5EItELOdCR7kTmE,1322
8
+ lanscape/core/ip_parser.py,sha256=kn5H4ERitLnreRAqifWphwbxdjItGqwu50lsMCPDMcA,3474
9
+ lanscape/core/logger.py,sha256=nzo6J8UdlMdhRkOJEDOIHKztoE3Du8PQZad7ixvNgeM,2534
10
+ lanscape/core/mac_lookup.py,sha256=PxBSMe3wEVDtivCsh5NclSAguZz9rqdAS7QshBiuWvM,3519
11
+ lanscape/core/net_tools.py,sha256=PsyH-I6ylrkJaVas_YUttYPhU2Cs8I5uCLibPzXxuoM,22076
12
+ lanscape/core/port_manager.py,sha256=3_ROOb6JEiB0NByZVtADuGcldFkgZwn1RKtvwgs9AIk,4479
13
+ lanscape/core/runtime_args.py,sha256=vZNDqb75hr3OQccgJip3XtYYljwa1tIzXQ5PdzHeIDg,2865
14
+ lanscape/core/scan_config.py,sha256=A2ZKXqXKW9nrP6yLb7b9b3XqSY_cQB3LZ5K0LVCSebE,11114
15
+ lanscape/core/service_scan.py,sha256=umDVOoCNNVJhWMtxDV-rn7hcS7t_V073srSMPTl5gMo,7943
16
+ lanscape/core/subnet_scan.py,sha256=PtSOk92dK05-reyr8LBkOXaI15qpYnar5nDqALCX1tQ,14850
17
+ lanscape/core/version_manager.py,sha256=eGjyKgsv31QO0W26se9pPQ1TwmEN8qn37dHULtoocqc,2841
18
+ lanscape/resources/mac_addresses/convert_csv.py,sha256=hvlyLs0XmuuhBuvXBNRGP1cKJzYVRSf8VfUJ1VqROms,1189
19
+ lanscape/resources/mac_addresses/mac_db.json,sha256=Lng2yJerwO7vjefzxzgtE203hry1lIsCadHL1A5Rptg,2136137
20
+ lanscape/resources/ports/convert_csv.py,sha256=MMLDa-5pGMsn4A2_k45jHsRYffrRY_0Z2D1ziiikeQA,1143
21
+ lanscape/resources/ports/full.json,sha256=O8XBW52QvEVSGMQDbXe4-c4qq6XAecw6KJW4m2HkTLo,1441444
22
+ lanscape/resources/ports/large.json,sha256=CzlCcIGCBW1QAgjz4NDerCYA8HcYf6lNxehh7F928y0,138410
23
+ lanscape/resources/ports/medium.json,sha256=T5Rc7wa47MtroHxuZrHSftOqRWbQzhZULJdE1vpsTvU,3518
24
+ lanscape/resources/ports/small.json,sha256=F_lo_5xHwHBfOVfVgxP7ejblR3R62SNtC1Mm33brhYc,376
25
+ lanscape/resources/ports/test_port_list_scan.json,sha256=qXuWGQ_sGIRCVrhJxMeWcHKYdjaMv8O6OVWutiqCwVo,36
26
+ lanscape/resources/services/definitions.jsonc,sha256=M9BDeK-mh25sEVj8xDEYbU2ix7EETVWhbiYmMb14Gjg,20905
27
+ lanscape/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ lanscape/ui/app.py,sha256=rg4UGHgbVHpU2jdabSwBoSqGna7WGOunPkPc5Tvds9w,4076
29
+ lanscape/ui/main.py,sha256=cL3og5eO-SdyX-ev-DhcuvO7htn5u50TtRTGv2HeK-A,5047
30
+ lanscape/ui/shutdown_handler.py,sha256=HrEnWrdYSzLDVsPgD8tf9FgtAwQrZNECDu6wnEs27EY,1704
31
+ lanscape/ui/blueprints/__init__.py,sha256=EjPtaR5Nh17pGiOVYTJULVNaZntpFZOiYyep8rBWAiE,265
32
+ lanscape/ui/blueprints/api/__init__.py,sha256=5Z4Y7B36O-bNFenpomfuNhPuJ9dW_MC0TPUU3pCFVfA,103
33
+ lanscape/ui/blueprints/api/port.py,sha256=enqJ87hpVHSTBoaV9bJ7IgsvHrWDTga704wV33dtwUg,2384
34
+ lanscape/ui/blueprints/api/scan.py,sha256=2rsW4xkI4X2Q2ocwaE469aU1VxQ3xHuBRjD9xE36WdI,3326
35
+ lanscape/ui/blueprints/api/tools.py,sha256=jr9gt0VhvBFgJ61MLgNIM6hin-MUmJLGdmStPx3e3Yc,2432
36
+ lanscape/ui/blueprints/web/__init__.py,sha256=NvgnjP0X4LwqVhSEyh5RUzoG45N44kHK1MEFlfvBxTg,118
37
+ lanscape/ui/blueprints/web/routes.py,sha256=f5TzfTzelJ_erslyBXTOpFr4BlIfB1Mb1ye6ioH7IL0,4534
38
+ lanscape/ui/static/lanscape.webmanifest,sha256=07CqA-PQsO35KJD8R96sI3Pxix6UuBjijPDCuy9vM3s,446
39
+ lanscape/ui/static/css/style.css,sha256=UWu0fOCVhNKRtKawaCy4hp-dU5Q-ac2WiSdIwRQMvFs,21442
40
+ lanscape/ui/static/img/ico/android-chrome-192x192.png,sha256=JmFT6KBCCuoyxMV-mLNtF9_QJbVBvfWPUizKN700fi8,18255
41
+ lanscape/ui/static/img/ico/android-chrome-512x512.png,sha256=88Jjx_1-4XAnZYz64KP6FdTl_kYkNG2_kQIKteQwSh4,138055
42
+ lanscape/ui/static/img/ico/apple-touch-icon.png,sha256=tEJlLwBZtF4v-NC90YCfRJQ2prTsF4i3VQLK_hnv2Mw,16523
43
+ lanscape/ui/static/img/ico/favicon-16x16.png,sha256=HpQOZk3rziZjT1xQxKuy5WourXsfrdwuzQY1hChzBJQ,573
44
+ lanscape/ui/static/img/ico/favicon-32x32.png,sha256=UpgiDPIHckK19udHtACiaI3ZPbmImUUcN1GcrjpEg9s,1302
45
+ lanscape/ui/static/img/ico/favicon.ico,sha256=rs5vq0MPJ1LzzioOzOz5aQLVfrtS2nLRc920dOeReTw,15406
46
+ lanscape/ui/static/img/ico/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
47
+ lanscape/ui/static/js/core.js,sha256=TzyoD1NCnIY-2JLArymw0JoMOq0CMeJrZDmZyMdMK2I,1394
48
+ lanscape/ui/static/js/layout-sizing.js,sha256=U2dsyJi-YKpOpudu3kg2whiU4047ghzDTY3ExYUhpPs,810
49
+ lanscape/ui/static/js/main.js,sha256=Bgb5Ld_UPWzBVzxWkLD0kJGjOoLJyzni2TKE6Uk99VA,7428
50
+ lanscape/ui/static/js/on-tab-close.js,sha256=3icxYWlLpY81iLoW7kQTJeWQ3UnyyboG0dESHF2wLPQ,1376
51
+ lanscape/ui/static/js/quietReload.js,sha256=T7lvxVejMGJr1cKdABedyiaTVukZfRDIKV7Eyv-jeyc,802
52
+ lanscape/ui/static/js/scan-config.js,sha256=nQ2dWkgpf8yer-lCMTZ25DBqOuWt0SIyzzfdP_fS-bE,8086
53
+ lanscape/ui/static/js/shutdown-server.js,sha256=Mx8UGmmktHaCK7DL8TVUxah6VEcN0wwLFfhbCId-K8U,453
54
+ lanscape/ui/static/js/subnet-info.js,sha256=osZM6CGs-TC5QpBJWkNWCtXNOKzjyIiWKHwKi4vlDf8,559
55
+ lanscape/ui/static/js/subnet-selector.js,sha256=2YKCAuKU2Ti1CmJrqi4_vNTD2LQbxx7chIDqND_1eAY,358
56
+ lanscape/ui/templates/base.html,sha256=HIHLFEUMDTGEaeuSpeNg-vkXXb9VNbZnbFWlYetVL18,1377
57
+ lanscape/ui/templates/error.html,sha256=bqGJbf_ix9wtpUlXk5zvz_XyFpeTbEO-4f0ImgLtUGk,1033
58
+ lanscape/ui/templates/info.html,sha256=SQ6RpTs9_v9HF32mr3FBsh6vTJneYqFz_WrC9diXzHg,2958
59
+ lanscape/ui/templates/main.html,sha256=CVBwJCNBjzOTFDCiIicT1wuptjCUfZzMc7YSvu4OXxA,3626
60
+ lanscape/ui/templates/scan.html,sha256=00QX2_1S_1wGzk42r00LjEkJvoioCLs6JgjOibi6r20,376
61
+ lanscape/ui/templates/shutdown.html,sha256=iXVCq2yl5TjZfNFl4esbDJra3gJA2VQpae0jj4ipy9w,701
62
+ lanscape/ui/templates/core/head.html,sha256=zP1RkTYuaKCC6RtnSEHFKPw3wKWfSyV0HZg5XsAxWik,719
63
+ lanscape/ui/templates/core/scripts.html,sha256=rSRi4Ut8iejajMPhOc5bzEz-Z3EHxpj_3PxwwyyhmTQ,640
64
+ lanscape/ui/templates/scan/config.html,sha256=vI5ZuJLZE5FwbTRkQyXwIRP-bwe_cZdSyzDZVZu1s_w,14821
65
+ lanscape/ui/templates/scan/device-detail.html,sha256=3N0WcdnWopbSFwsnKogBaHOYsLMAfKBZdkP7HQG4vLA,4794
66
+ lanscape/ui/templates/scan/export.html,sha256=Nvs_unojzT3qhN_ZnEgYHou2C9wqWGr3dVr2UiLnYjY,749
67
+ lanscape/ui/templates/scan/ip-table-row.html,sha256=viAvjJcye3jYpDsWGR2To8BGqr1NxuF3gPO2ECQnxUU,1302
68
+ lanscape/ui/templates/scan/ip-table.html,sha256=8pkaStlVeGJ3h6spk8eKu3Ft5HffH7KpHD3vjECyHa0,977
69
+ lanscape/ui/templates/scan/overview.html,sha256=xWj9jWDPg2KcPLvS8fnSins23_UXjKCdb2NJwNG2U2Q,1176
70
+ lanscape/ui/templates/scan/scan-error.html,sha256=wmAYQ13IJHUoO8fAGNDjMvNml7tu4rsIU3Vav71ETlA,999
71
+ lanscape/ui/ws/__init__.py,sha256=kwvrI6WAgxRMToI3NAHdDph4FtbvR7cWODh4WrXHoxg,688
72
+ lanscape/ui/ws/delta.py,sha256=sMmjG8darYosdOds1Sdl2MqwqG44P6Ypb27FJnMz0-E,4798
73
+ lanscape/ui/ws/protocol.py,sha256=i6ULVg-hNyXvKRUKEDJHa2_RfMunieEU8VIYTNXxfD0,2135
74
+ lanscape/ui/ws/server.py,sha256=-rAiWOmPS5UOy5TA9eTAd0oBGLM988qW9b5SWqQNuso,11734
75
+ lanscape/ui/ws/handlers/__init__.py,sha256=D3m4CvaVl-TURkBqADmLaEoBvGcHSIRnFzsBcwXkUy8,544
76
+ lanscape/ui/ws/handlers/base.py,sha256=Uu0FUBSG4F_2SsqORushu132EF_6mo-7uYhJTNu9SpI,4051
77
+ lanscape/ui/ws/handlers/port.py,sha256=0bFFP_BtoHbmg0O5iaCKtVW08GNcrlc3DMhVUixaGT0,5397
78
+ lanscape/ui/ws/handlers/scan.py,sha256=OniNOp-RmmkhlcOpnll4BVYOWnxyAJH7VP2owImsAhk,10977
79
+ lanscape/ui/ws/handlers/tools.py,sha256=Un24y8YqRUHzr2KW4S7IG5Tdl3nu8hHoxVDF8E3H6yM,4533
80
+ lanscape-2.4.0a2.dist-info/licenses/LICENSE,sha256=VLoE0IrNTIc09dFm7hMN0qzk4T3q8V0NaPcFQqMemDs,1070
81
+ lanscape-2.4.0a2.dist-info/METADATA,sha256=ruSveDjVkHapd_gA5CNPTxIwFFj5sznyl4HZ2iS31Bw,3793
82
+ lanscape-2.4.0a2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
83
+ lanscape-2.4.0a2.dist-info/entry_points.txt,sha256=evxSxUikFa1OEd4e0Boky9sLH87HdgM0YqB_AbB2HYc,51
84
+ lanscape-2.4.0a2.dist-info/top_level.txt,sha256=E9D4sjPz_6H7c85Ycy_pOS2xuv1Wm-ilKhxEprln2ps,9
85
+ lanscape-2.4.0a2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lanscape = lanscape.ui.main:main
@@ -1,170 +0,0 @@
1
-
2
- """
3
- Decorators and job tracking utilities for Lanscape.
4
- """
5
-
6
- from time import time
7
- from dataclasses import dataclass, field
8
- from typing import DefaultDict
9
- from collections import defaultdict
10
- import inspect
11
- import functools
12
- import concurrent.futures
13
- from tabulate import tabulate
14
-
15
-
16
- @dataclass
17
- class JobStats:
18
- """
19
- Tracks statistics for job execution, including running, finished, and timing data.
20
- """
21
- running: DefaultDict[str, int] = field(
22
- default_factory=lambda: defaultdict(int))
23
- finished: DefaultDict[str, int] = field(
24
- default_factory=lambda: defaultdict(int))
25
- timing: DefaultDict[str, float] = field(
26
- default_factory=lambda: defaultdict(float))
27
-
28
- _instance = None
29
-
30
- def __init__(self):
31
- # Only initialize once
32
- if not hasattr(self, "running"):
33
- self.running = defaultdict(int)
34
- self.finished = defaultdict(int)
35
- self.timing = defaultdict(float)
36
-
37
- def __new__(cls, *args, **kwargs):
38
- if cls._instance is None:
39
- cls._instance = super(JobStats, cls).__new__(cls)
40
- return cls._instance
41
-
42
- def __str__(self):
43
- """Return a formatted string representation of the job statistics."""
44
- data = [
45
- [
46
- name,
47
- self.running.get(name, 0),
48
- self.finished.get(name, 0),
49
- self.timing.get(name, 0.0)
50
- ]
51
- for name in set(self.running) | set(self.finished)
52
- ]
53
- headers = ["Function", "Running", "Finished", "Avg Time (s)"]
54
- return tabulate(
55
- data,
56
- headers=headers,
57
- tablefmt="grid"
58
- )
59
-
60
-
61
- class JobStatsMixin: # pylint: disable=too-few-public-methods
62
- """
63
- Singleton mixin that provides shared job_stats property across all instances.
64
- """
65
- _job_stats = None
66
-
67
- @property
68
- def job_stats(self):
69
- """Return the shared JobStats instance."""
70
- return JobStats()
71
-
72
-
73
- def job_tracker(func):
74
- """
75
- Decorator to track job statistics for a method,
76
- including running count, finished count, and average timing.
77
- """
78
- def get_fxn_src_name(func, first_arg) -> str:
79
- """
80
- Return the function name with the class name prepended if available.
81
- """
82
- qual_parts = func.__qualname__.split(".")
83
- cls_name = qual_parts[-2] if len(qual_parts) > 1 else None
84
- cls_obj = None # resolved lazily
85
- if cls_obj is None and cls_name:
86
- mod = inspect.getmodule(func)
87
- cls_obj = getattr(mod, cls_name, None)
88
- if cls_obj and first_arg is not None:
89
- if (first_arg is cls_obj or isinstance(first_arg, cls_obj)):
90
- return f"{cls_name}.{func.__name__}"
91
- return func.__name__
92
-
93
- def wrapper(*args, **kwargs):
94
- """Wrap the function to update job statistics before and after execution."""
95
- class_instance = args[0]
96
- job_stats = JobStats()
97
- fxn = get_fxn_src_name(
98
- func,
99
- class_instance
100
- )
101
-
102
- # Increment running counter and track execution time
103
- job_stats.running[fxn] += 1
104
- start = time()
105
-
106
- result = func(*args, **kwargs) # Execute the wrapped function
107
-
108
- # Update statistics after function execution
109
- elapsed = time() - start
110
- job_stats.running[fxn] -= 1
111
- job_stats.finished[fxn] += 1
112
-
113
- # Calculate the new average timing for the function
114
- job_stats.timing[fxn] = round(
115
- ((job_stats.finished[fxn] - 1) * job_stats.timing[fxn] + elapsed)
116
- / job_stats.finished[fxn],
117
- 4
118
- )
119
-
120
- # Clean up if no more running instances of this function
121
- if job_stats.running[fxn] == 0:
122
- job_stats.running.pop(fxn)
123
-
124
- return result
125
-
126
- return wrapper
127
-
128
-
129
- def terminator(func):
130
- """
131
- Decorator designed specifically for the SubnetScanner class,
132
- helps facilitate termination of a job.
133
- """
134
- def wrapper(*args, **kwargs):
135
- """Wrap the function to check if the scan is running before execution."""
136
- scan = args[0] # aka self
137
- if not scan.running:
138
- return None
139
- return func(*args, **kwargs)
140
-
141
- return wrapper
142
-
143
-
144
- def timeout_enforcer(timeout: int, raise_on_timeout: bool = True):
145
- """
146
- Decorator to enforce a timeout on a function.
147
-
148
- Args:
149
- timeout (int): Timeout length in seconds.
150
- raise_on_timeout (bool): Whether to raise an exception if the timeout is exceeded.
151
- """
152
- def decorator(func):
153
- @functools.wraps(func)
154
- def wrapper(*args, **kwargs):
155
- """Wrap the function to enforce a timeout on its execution."""
156
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
157
- future = executor.submit(func, *args, **kwargs)
158
- try:
159
- return future.result(
160
- timeout=timeout
161
- )
162
- except concurrent.futures.TimeoutError as exc:
163
- if raise_on_timeout:
164
- raise TimeoutError(
165
- f"Function '{func.__name__}' exceeded timeout of "
166
- f"{timeout} seconds."
167
- ) from exc
168
- return None # Return None if not raising an exception
169
- return wrapper
170
- return decorator