lanscape 2.3.1__tar.gz → 2.4.0b1__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.
Files changed (102) hide show
  1. {lanscape-2.3.1/lanscape.egg-info → lanscape-2.4.0b1}/PKG-INFO +3 -1
  2. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/runtime_args.py +6 -0
  3. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/main.py +22 -0
  4. lanscape-2.4.0b1/lanscape/ui/ws/__init__.py +31 -0
  5. lanscape-2.4.0b1/lanscape/ui/ws/delta.py +170 -0
  6. lanscape-2.4.0b1/lanscape/ui/ws/handlers/__init__.py +20 -0
  7. lanscape-2.4.0b1/lanscape/ui/ws/handlers/base.py +145 -0
  8. lanscape-2.4.0b1/lanscape/ui/ws/handlers/port.py +184 -0
  9. lanscape-2.4.0b1/lanscape/ui/ws/handlers/scan.py +352 -0
  10. lanscape-2.4.0b1/lanscape/ui/ws/handlers/tools.py +145 -0
  11. lanscape-2.4.0b1/lanscape/ui/ws/protocol.py +86 -0
  12. lanscape-2.4.0b1/lanscape/ui/ws/server.py +375 -0
  13. {lanscape-2.3.1 → lanscape-2.4.0b1/lanscape.egg-info}/PKG-INFO +3 -1
  14. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape.egg-info/SOURCES.txt +11 -1
  15. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape.egg-info/requires.txt +2 -0
  16. {lanscape-2.3.1 → lanscape-2.4.0b1}/pyproject.toml +4 -2
  17. lanscape-2.4.0b1/tests/test_websocket.py +940 -0
  18. {lanscape-2.3.1 → lanscape-2.4.0b1}/LICENSE +0 -0
  19. {lanscape-2.3.1 → lanscape-2.4.0b1}/MANIFEST.in +0 -0
  20. {lanscape-2.3.1 → lanscape-2.4.0b1}/README.md +0 -0
  21. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/__init__.py +0 -0
  22. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/__main__.py +0 -0
  23. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/__init__.py +0 -0
  24. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/app_scope.py +0 -0
  25. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/decorators.py +0 -0
  26. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/device_alive.py +0 -0
  27. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/errors.py +0 -0
  28. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/ip_parser.py +0 -0
  29. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/logger.py +0 -0
  30. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/mac_lookup.py +0 -0
  31. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/net_tools.py +0 -0
  32. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/port_manager.py +0 -0
  33. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/scan_config.py +0 -0
  34. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/service_scan.py +0 -0
  35. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/subnet_scan.py +0 -0
  36. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/core/version_manager.py +0 -0
  37. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/mac_addresses/convert_csv.py +0 -0
  38. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/mac_addresses/mac_db.json +0 -0
  39. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/convert_csv.py +0 -0
  40. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/full.json +0 -0
  41. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/large.json +0 -0
  42. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/medium.json +0 -0
  43. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/small.json +0 -0
  44. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/ports/test_port_list_scan.json +0 -0
  45. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/resources/services/definitions.jsonc +0 -0
  46. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/__init__.py +0 -0
  47. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/app.py +0 -0
  48. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/__init__.py +0 -0
  49. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/__init__.py +0 -0
  50. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/port.py +0 -0
  51. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/scan.py +0 -0
  52. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/api/tools.py +0 -0
  53. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/web/__init__.py +0 -0
  54. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/blueprints/web/routes.py +0 -0
  55. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/shutdown_handler.py +0 -0
  56. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/css/style.css +0 -0
  57. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/android-chrome-192x192.png +0 -0
  58. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/android-chrome-512x512.png +0 -0
  59. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/apple-touch-icon.png +0 -0
  60. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon-16x16.png +0 -0
  61. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon-32x32.png +0 -0
  62. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/favicon.ico +0 -0
  63. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/img/ico/site.webmanifest +0 -0
  64. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/core.js +0 -0
  65. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/layout-sizing.js +0 -0
  66. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/main.js +0 -0
  67. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/on-tab-close.js +0 -0
  68. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/quietReload.js +0 -0
  69. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/scan-config.js +0 -0
  70. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/shutdown-server.js +0 -0
  71. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/subnet-info.js +0 -0
  72. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/js/subnet-selector.js +0 -0
  73. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/static/lanscape.webmanifest +0 -0
  74. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/base.html +0 -0
  75. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/core/head.html +0 -0
  76. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/core/scripts.html +0 -0
  77. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/error.html +0 -0
  78. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/info.html +0 -0
  79. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/main.html +0 -0
  80. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/config.html +0 -0
  81. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/device-detail.html +0 -0
  82. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/export.html +0 -0
  83. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/ip-table-row.html +0 -0
  84. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/ip-table.html +0 -0
  85. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/overview.html +0 -0
  86. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan/scan-error.html +0 -0
  87. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/scan.html +0 -0
  88. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape/ui/templates/shutdown.html +0 -0
  89. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape.egg-info/dependency_links.txt +0 -0
  90. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape.egg-info/entry_points.txt +0 -0
  91. {lanscape-2.3.1 → lanscape-2.4.0b1}/lanscape.egg-info/top_level.txt +0 -0
  92. {lanscape-2.3.1 → lanscape-2.4.0b1}/setup.cfg +0 -0
  93. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_api.py +0 -0
  94. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_decorators.py +0 -0
  95. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_env.py +0 -0
  96. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_globals.py +0 -0
  97. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_library.py +0 -0
  98. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_logging.py +0 -0
  99. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_port_scan.py +0 -0
  100. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_port_scan_linux_fd_exhaustion.py +0 -0
  101. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_service_scan.py +0 -0
  102. {lanscape-2.3.1 → lanscape-2.4.0b1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lanscape
3
- Version: 2.3.1
3
+ Version: 2.4.0b1
4
4
  Summary: A python based local network scanner
5
5
  Author-email: Michael Dennis <michael@dipduo.com>
6
6
  License-Expression: MIT
@@ -26,10 +26,12 @@ Requires-Dist: tabulate==0.9.0
26
26
  Requires-Dist: pydantic
27
27
  Requires-Dist: icmplib
28
28
  Requires-Dist: pwa-launcher>=1.1.0
29
+ Requires-Dist: websockets<14.0,>=12.0
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest>=8.0; extra == "dev"
31
32
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
32
33
  Requires-Dist: pytest-xdist>=3.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
33
35
  Requires-Dist: openai>=1.0.0; extra == "dev"
34
36
  Requires-Dist: pylint>=3.0; extra == "dev"
35
37
  Requires-Dist: autopep8>=2.0; extra == "dev"
@@ -14,6 +14,8 @@ class RuntimeArgs:
14
14
  loglevel: str = 'INFO'
15
15
  flask_logging: bool = False
16
16
  persistent: bool = False
17
+ ws_server: bool = False
18
+ ws_port: int = 8766
17
19
 
18
20
 
19
21
  def parse_args() -> RuntimeArgs:
@@ -35,6 +37,10 @@ def parse_args() -> RuntimeArgs:
35
37
  help='Don\'t exit after browser is closed')
36
38
  parser.add_argument('--debug', action='store_true',
37
39
  help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
40
+ parser.add_argument('--ws-server', action='store_true',
41
+ help='Start WebSocket server instead of Flask UI')
42
+ parser.add_argument('--ws-port', type=int, default=8766,
43
+ help='Port for WebSocket server (default: 8766)')
38
44
 
39
45
  # Parse the arguments
40
46
  args = parser.parse_args()
@@ -16,6 +16,7 @@ from lanscape.core.logger import configure_logging
16
16
  from lanscape.core.runtime_args import parse_args
17
17
  from lanscape.core.version_manager import get_installed_version, is_update_available
18
18
  from lanscape.ui.app import start_webserver_daemon, start_webserver
19
+ from lanscape.ui.ws.server import run_server
19
20
  # do this so any logs generated on import are displayed
20
21
  args = parse_args()
21
22
  configure_logging(args.loglevel, args.logfile, args.flask_logging)
@@ -48,6 +49,11 @@ def _main():
48
49
  else:
49
50
  log.info('Flask reloaded app.')
50
51
 
52
+ # Check if WebSocket server mode is requested
53
+ if args.ws_server:
54
+ start_websocket_server()
55
+ return
56
+
51
57
  args.port = get_valid_port(args.port)
52
58
 
53
59
  try:
@@ -72,6 +78,22 @@ def try_check_update():
72
78
  log.warning('Unable to check for updates.')
73
79
 
74
80
 
81
+ def start_websocket_server():
82
+ """Start the WebSocket server."""
83
+ args.ws_port = get_valid_port(args.ws_port)
84
+ log.info(f'Starting WebSocket server on port {args.ws_port}')
85
+ log.info(f'React UI should connect to ws://localhost:{args.ws_port}')
86
+
87
+ try:
88
+ run_server(host='0.0.0.0', port=args.ws_port)
89
+ except KeyboardInterrupt:
90
+ log.info('WebSocket server stopped by user')
91
+ except Exception as e:
92
+ log.critical(f'WebSocket server failed: {e}')
93
+ log.debug(traceback.format_exc())
94
+ raise
95
+
96
+
75
97
  def open_browser(url: str, wait=2) -> Popen | None:
76
98
  """
77
99
  Open a browser window to the specified
@@ -0,0 +1,31 @@
1
+ """
2
+ WebSocket interface for LANscape.
3
+
4
+ Provides a standalone WebSocket server that exposes all LANscape functionality,
5
+ allowing clients to initiate scans, manage port lists, and receive real-time
6
+ scan results with delta updates.
7
+ """
8
+
9
+ from lanscape.ui.ws.server import WebSocketServer, run_server
10
+ from lanscape.ui.ws.protocol import (
11
+ WSMessage,
12
+ WSRequest,
13
+ WSResponse,
14
+ WSError,
15
+ WSEvent,
16
+ MessageType
17
+ )
18
+ from lanscape.ui.ws.delta import DeltaTracker, ScanDeltaTracker
19
+
20
+ __all__ = [
21
+ 'WebSocketServer',
22
+ 'run_server',
23
+ 'WSMessage',
24
+ 'WSRequest',
25
+ 'WSResponse',
26
+ 'WSError',
27
+ 'WSEvent',
28
+ 'MessageType',
29
+ 'DeltaTracker',
30
+ 'ScanDeltaTracker'
31
+ ]
@@ -0,0 +1,170 @@
1
+ """
2
+ Delta tracking for efficient scan result updates.
3
+
4
+ Uses content hashing to detect changes and only send updated data
5
+ to clients, reducing bandwidth and improving performance.
6
+ """
7
+
8
+ import json
9
+ import hashlib
10
+ from typing import Any, Optional
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class DeltaState(BaseModel):
16
+ """
17
+ Represents the state of a tracked item.
18
+
19
+ Attributes:
20
+ hash: Content hash of the serialized data
21
+ data: The actual data being tracked
22
+ """
23
+ hash: str
24
+ data: Any
25
+
26
+
27
+ class DeltaTracker:
28
+ """
29
+ Tracks changes to scan results and provides delta updates.
30
+
31
+ Uses MD5 hashing to detect changes in device data and scan state.
32
+ Clients receive only the changed portions of scan results.
33
+ """
34
+
35
+ def __init__(self):
36
+ """Initialize the delta tracker with empty state."""
37
+ self._states: dict[str, DeltaState] = {}
38
+
39
+ @staticmethod
40
+ def compute_hash(data: Any) -> str:
41
+ """
42
+ Compute MD5 hash of serialized data.
43
+
44
+ Args:
45
+ data: Any JSON-serializable data
46
+
47
+ Returns:
48
+ Hex string of the MD5 hash
49
+ """
50
+ serialized = json.dumps(data, sort_keys=True, default=str)
51
+ return hashlib.md5(serialized.encode()).hexdigest()
52
+
53
+ def update(self, key: str, data: Any) -> Optional[Any]:
54
+ """
55
+ Update tracked state and return data if changed.
56
+
57
+ Args:
58
+ key: Unique identifier for the tracked item
59
+ data: Current data for the item
60
+
61
+ Returns:
62
+ The data if it has changed, None otherwise
63
+ """
64
+ new_hash = self.compute_hash(data)
65
+ current_state = self._states.get(key)
66
+
67
+ if current_state is None or current_state.hash != new_hash:
68
+ self._states[key] = DeltaState(hash=new_hash, data=data)
69
+ return data
70
+ return None
71
+
72
+ def get_changes(self, items: dict[str, Any]) -> dict[str, Any]:
73
+ """
74
+ Get only changed items from a dictionary.
75
+
76
+ Args:
77
+ items: Dictionary of key -> data to check for changes
78
+
79
+ Returns:
80
+ Dictionary containing only the changed items
81
+ """
82
+ changes = {}
83
+ for key, data in items.items():
84
+ result = self.update(key, data)
85
+ if result is not None:
86
+ changes[key] = result
87
+ return changes
88
+
89
+ def reset(self, key: Optional[str] = None) -> None:
90
+ """
91
+ Reset tracked state.
92
+
93
+ Args:
94
+ key: Specific key to reset, or None to reset all
95
+ """
96
+ if key is not None:
97
+ self._states.pop(key, None)
98
+ else:
99
+ self._states.clear()
100
+
101
+ def has_key(self, key: str) -> bool:
102
+ """
103
+ Check if a key is being tracked.
104
+
105
+ Args:
106
+ key: The key to check
107
+
108
+ Returns:
109
+ True if the key is tracked, False otherwise
110
+ """
111
+ return key in self._states
112
+
113
+ def get_hash(self, key: str) -> Optional[str]:
114
+ """
115
+ Get the current hash for a tracked key.
116
+
117
+ Args:
118
+ key: The key to get the hash for
119
+
120
+ Returns:
121
+ The current hash, or None if not tracked
122
+ """
123
+ state = self._states.get(key)
124
+ return state.hash if state else None
125
+
126
+
127
+ class ScanDeltaTracker(DeltaTracker):
128
+ """
129
+ Specialized delta tracker for scan results.
130
+
131
+ Tracks individual devices and overall scan metadata,
132
+ providing efficient delta updates for real-time scan monitoring.
133
+ """
134
+
135
+ def get_scan_delta(self, scan_results: dict) -> dict:
136
+ """
137
+ Get delta update for scan results.
138
+
139
+ Args:
140
+ scan_results: Full scan results dictionary
141
+
142
+ Returns:
143
+ Dictionary containing only changed fields:
144
+ - 'devices': List of changed device data
145
+ - 'metadata': Changed scan metadata (if any)
146
+ - 'has_changes': Boolean indicating if there are any changes
147
+ """
148
+ delta = {
149
+ 'devices': [],
150
+ 'metadata': None,
151
+ 'has_changes': False
152
+ }
153
+
154
+ # Track metadata changes (everything except devices)
155
+ metadata = {k: v for k, v in scan_results.items() if k != 'devices'}
156
+ metadata_change = self.update('_metadata', metadata)
157
+ if metadata_change is not None:
158
+ delta['metadata'] = metadata_change
159
+ delta['has_changes'] = True
160
+
161
+ # Track individual device changes
162
+ devices = scan_results.get('devices', [])
163
+ for device in devices:
164
+ device_ip = device.get('ip', str(id(device)))
165
+ device_change = self.update(f'device_{device_ip}', device)
166
+ if device_change is not None:
167
+ delta['devices'].append(device_change)
168
+ delta['has_changes'] = True
169
+
170
+ return delta
@@ -0,0 +1,20 @@
1
+ """
2
+ WebSocket handlers for LANscape.
3
+
4
+ Provides handler classes for different functional areas:
5
+ - ScanHandler: Network scanning operations
6
+ - PortHandler: Port list management
7
+ - ToolsHandler: Utility functions (subnet validation, etc.)
8
+ """
9
+
10
+ from lanscape.ui.ws.handlers.base import BaseHandler
11
+ from lanscape.ui.ws.handlers.scan import ScanHandler
12
+ from lanscape.ui.ws.handlers.port import PortHandler
13
+ from lanscape.ui.ws.handlers.tools import ToolsHandler
14
+
15
+ __all__ = [
16
+ 'BaseHandler',
17
+ 'ScanHandler',
18
+ 'PortHandler',
19
+ 'ToolsHandler'
20
+ ]
@@ -0,0 +1,145 @@
1
+ """
2
+ Base handler class for WebSocket handlers.
3
+
4
+ Provides common functionality and interface for all handlers.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import traceback
10
+ from typing import Any, Callable, Optional
11
+
12
+ from lanscape.ui.ws.protocol import WSRequest, WSResponse, WSError
13
+
14
+
15
+ class BaseHandler:
16
+ """
17
+ Base class for WebSocket message handlers.
18
+
19
+ Provides registration of action handlers and dispatch logic.
20
+ Subclasses should register their handlers in __init__.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize the handler with an empty action registry."""
25
+ self._actions: dict[str, Callable] = {}
26
+ self.log = logging.getLogger(self.__class__.__name__)
27
+
28
+ @property
29
+ def prefix(self) -> str:
30
+ """
31
+ The action prefix for this handler.
32
+
33
+ Returns:
34
+ String prefix (e.g., 'scan', 'port', 'tools')
35
+ """
36
+ raise NotImplementedError("Subclasses must define a prefix")
37
+
38
+ def register(self, action: str, handler: Callable) -> None:
39
+ """
40
+ Register an action handler.
41
+
42
+ Args:
43
+ action: The action name (without prefix)
44
+ handler: The callable to handle the action
45
+ """
46
+ full_action = f"{self.prefix}.{action}"
47
+ self._actions[full_action] = handler
48
+ self.log.debug(f"Registered handler for action: {full_action}")
49
+
50
+ def can_handle(self, action: str) -> bool:
51
+ """
52
+ Check if this handler can process the given action.
53
+
54
+ Args:
55
+ action: The full action name
56
+
57
+ Returns:
58
+ True if this handler can process the action
59
+ """
60
+ return action in self._actions
61
+
62
+ def get_actions(self) -> list[str]:
63
+ """
64
+ Get all registered actions.
65
+
66
+ Returns:
67
+ List of action names this handler supports
68
+ """
69
+ return list(self._actions.keys())
70
+
71
+ async def handle(
72
+ self,
73
+ request: WSRequest,
74
+ send_event: Optional[Callable] = None
75
+ ) -> WSResponse | WSError:
76
+ """
77
+ Handle a WebSocket request.
78
+
79
+ Args:
80
+ request: The incoming request
81
+ send_event: Optional callback to send events to the client
82
+
83
+ Returns:
84
+ WSResponse on success, WSError on failure
85
+ """
86
+ action = request.action
87
+ handler = self._actions.get(action)
88
+
89
+ if handler is None:
90
+ return WSError(
91
+ id=request.id,
92
+ action=action,
93
+ error=f"Unknown action: {action}"
94
+ )
95
+
96
+ try:
97
+ params = request.params or {}
98
+ # Check if handler is async
99
+ if asyncio.iscoroutinefunction(handler):
100
+ result = await handler(params, send_event)
101
+ else:
102
+ result = handler(params, send_event)
103
+
104
+ return WSResponse(
105
+ id=request.id,
106
+ action=action,
107
+ data=result,
108
+ success=True
109
+ )
110
+ except Exception as e:
111
+ self.log.error(f"Error handling {action}: {e}")
112
+ self.log.debug(traceback.format_exc())
113
+ return WSError(
114
+ id=request.id,
115
+ action=action,
116
+ error=str(e),
117
+ traceback=traceback.format_exc()
118
+ )
119
+
120
+ def _get_param(
121
+ self,
122
+ params: dict[str, Any],
123
+ key: str,
124
+ required: bool = False,
125
+ default: Any = None
126
+ ) -> Any:
127
+ """
128
+ Get a parameter from the params dict with optional validation.
129
+
130
+ Args:
131
+ params: The parameters dictionary
132
+ key: The parameter key
133
+ required: Whether the parameter is required
134
+ default: Default value if not present
135
+
136
+ Returns:
137
+ The parameter value
138
+
139
+ Raises:
140
+ ValueError: If required parameter is missing
141
+ """
142
+ value = params.get(key, default)
143
+ if required and value is None:
144
+ raise ValueError(f"Missing required parameter: {key}")
145
+ return value
@@ -0,0 +1,184 @@
1
+ """
2
+ WebSocket handler for port list management.
3
+
4
+ Provides handlers for:
5
+ - Listing port lists
6
+ - Getting port list details
7
+ - Creating, updating, deleting port lists
8
+ """
9
+
10
+ from typing import Any, Callable, Optional
11
+
12
+ from lanscape.core.port_manager import PortManager
13
+ from lanscape.ui.ws.handlers.base import BaseHandler
14
+
15
+
16
+ class PortHandler(BaseHandler):
17
+ """
18
+ Handler for port list management WebSocket actions.
19
+
20
+ Supports actions:
21
+ - port.list: Get all port list names
22
+ - port.list_summary: Get port lists with port counts
23
+ - port.get: Get a specific port list
24
+ - port.create: Create a new port list
25
+ - port.update: Update an existing port list
26
+ - port.delete: Delete a port list
27
+ """
28
+
29
+ def __init__(self, port_manager: Optional[PortManager] = None):
30
+ """
31
+ Initialize the port handler.
32
+
33
+ Args:
34
+ port_manager: Optional PortManager instance.
35
+ """
36
+ super().__init__()
37
+ self._port_manager = port_manager or PortManager()
38
+
39
+ # Register handlers
40
+ self.register('list', self._handle_list)
41
+ self.register('list_summary', self._handle_list_summary)
42
+ self.register('get', self._handle_get)
43
+ self.register('create', self._handle_create)
44
+ self.register('update', self._handle_update)
45
+ self.register('delete', self._handle_delete)
46
+
47
+ @property
48
+ def prefix(self) -> str:
49
+ """Return the action prefix for this handler."""
50
+ return 'port'
51
+
52
+ def _handle_list(
53
+ self,
54
+ params: dict[str, Any], # pylint: disable=unused-argument
55
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
56
+ ) -> list:
57
+ """
58
+ Get all available port lists.
59
+
60
+ Returns:
61
+ List of port list names
62
+ """
63
+ return self._port_manager.get_port_lists()
64
+
65
+ def _handle_list_summary(
66
+ self,
67
+ params: dict[str, Any], # pylint: disable=unused-argument
68
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
69
+ ) -> list:
70
+ """
71
+ Get port list names with their port counts.
72
+
73
+ Returns:
74
+ List of dicts with 'name' and 'count' keys
75
+ """
76
+ summaries = []
77
+ for name in self._port_manager.get_port_lists():
78
+ ports = self._port_manager.get_port_list(name) or {}
79
+ summaries.append({
80
+ 'name': name,
81
+ 'count': len(ports)
82
+ })
83
+ return summaries
84
+
85
+ def _handle_get(
86
+ self,
87
+ params: dict[str, Any],
88
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
89
+ ) -> dict:
90
+ """
91
+ Get a specific port list by name.
92
+
93
+ Params:
94
+ name: Name of the port list to retrieve
95
+
96
+ Returns:
97
+ Dict mapping port numbers to service names
98
+ """
99
+ name = self._get_param(params, 'name', required=True)
100
+ port_list = self._port_manager.get_port_list(name)
101
+
102
+ if port_list is None:
103
+ raise ValueError(f"Port list not found: {name}")
104
+
105
+ return port_list
106
+
107
+ def _handle_create(
108
+ self,
109
+ params: dict[str, Any],
110
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
111
+ ) -> dict:
112
+ """
113
+ Create a new port list.
114
+
115
+ Params:
116
+ name: Name for the new port list
117
+ ports: Dict mapping port numbers to service names
118
+
119
+ Returns:
120
+ Dict with success status
121
+ """
122
+ name = self._get_param(params, 'name', required=True)
123
+ ports = self._get_param(params, 'ports', required=True)
124
+
125
+ success = self._port_manager.create_port_list(name, ports)
126
+
127
+ if not success:
128
+ raise ValueError(
129
+ f"Failed to create port list '{name}'. "
130
+ "It may already exist or have invalid port data."
131
+ )
132
+
133
+ return {'success': True, 'name': name}
134
+
135
+ def _handle_update(
136
+ self,
137
+ params: dict[str, Any],
138
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
139
+ ) -> dict:
140
+ """
141
+ Update an existing port list.
142
+
143
+ Params:
144
+ name: Name of the port list to update
145
+ ports: New dict mapping port numbers to service names
146
+
147
+ Returns:
148
+ Dict with success status
149
+ """
150
+ name = self._get_param(params, 'name', required=True)
151
+ ports = self._get_param(params, 'ports', required=True)
152
+
153
+ success = self._port_manager.update_port_list(name, ports)
154
+
155
+ if not success:
156
+ raise ValueError(
157
+ f"Failed to update port list '{name}'. "
158
+ "It may not exist or have invalid port data."
159
+ )
160
+
161
+ return {'success': True, 'name': name}
162
+
163
+ def _handle_delete(
164
+ self,
165
+ params: dict[str, Any],
166
+ send_event: Optional[Callable] = None # pylint: disable=unused-argument
167
+ ) -> dict:
168
+ """
169
+ Delete a port list.
170
+
171
+ Params:
172
+ name: Name of the port list to delete
173
+
174
+ Returns:
175
+ Dict with success status
176
+ """
177
+ name = self._get_param(params, 'name', required=True)
178
+
179
+ success = self._port_manager.delete_port_list(name)
180
+
181
+ if not success:
182
+ raise ValueError(f"Failed to delete port list '{name}'. It may not exist.")
183
+
184
+ return {'success': True, 'name': name}