lanscape 1.3.5a1__py3-none-any.whl → 1.3.6a1__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 (36) hide show
  1. lanscape/__init__.py +9 -1
  2. lanscape/libraries/app_scope.py +0 -1
  3. lanscape/libraries/decorators.py +26 -9
  4. lanscape/libraries/device_alive.py +227 -0
  5. lanscape/libraries/errors.py +10 -0
  6. lanscape/libraries/ip_parser.py +73 -1
  7. lanscape/libraries/logger.py +29 -1
  8. lanscape/libraries/mac_lookup.py +5 -0
  9. lanscape/libraries/net_tools.py +156 -188
  10. lanscape/libraries/port_manager.py +83 -0
  11. lanscape/libraries/scan_config.py +173 -19
  12. lanscape/libraries/service_scan.py +3 -3
  13. lanscape/libraries/subnet_scan.py +111 -26
  14. lanscape/libraries/version_manager.py +50 -7
  15. lanscape/libraries/web_browser.py +75 -58
  16. lanscape/resources/mac_addresses/convert_csv.py +13 -2
  17. lanscape/resources/ports/convert_csv.py +13 -3
  18. lanscape/ui/app.py +24 -6
  19. lanscape/ui/blueprints/__init__.py +4 -1
  20. lanscape/ui/blueprints/api/__init__.py +2 -0
  21. lanscape/ui/blueprints/api/port.py +46 -0
  22. lanscape/ui/blueprints/api/scan.py +57 -5
  23. lanscape/ui/blueprints/api/tools.py +1 -0
  24. lanscape/ui/blueprints/web/__init__.py +4 -0
  25. lanscape/ui/blueprints/web/routes.py +52 -2
  26. lanscape/ui/main.py +1 -10
  27. lanscape/ui/shutdown_handler.py +5 -1
  28. lanscape/ui/static/css/style.css +35 -24
  29. lanscape/ui/static/js/scan-config.js +76 -2
  30. lanscape/ui/templates/main.html +0 -7
  31. lanscape/ui/templates/scan/config.html +71 -10
  32. {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/METADATA +1 -1
  33. {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/RECORD +36 -35
  34. {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/WHEEL +0 -0
  35. {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/licenses/LICENSE +0 -0
  36. {lanscape-1.3.5a1.dist-info → lanscape-1.3.6a1.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,27 @@
1
+ """
2
+ Configuration module for network scanning operations.
3
+ Provides classes and utilities to configure different types of network scans
4
+ including ping scans, ARP scans, and port scanning.
5
+ """
6
+
7
+ import os
1
8
  from typing import List, Dict
2
9
  import ipaddress
3
- from pydantic import BaseModel, Field
4
10
  from enum import Enum
5
11
 
12
+ from pydantic import BaseModel, Field
6
13
 
7
14
  from lanscape.libraries.port_manager import PortManager
8
15
  from lanscape.libraries.ip_parser import parse_ip_input
9
16
 
10
17
 
11
18
  class PingConfig(BaseModel):
19
+ """
20
+ Configuration settings for ICMP ping-based network scanning.
21
+
22
+ Controls parameters such as the number of ping attempts, count per ping,
23
+ timeout values, and retry delays to optimize ping scanning behavior.
24
+ """
12
25
  attempts: int = 2
13
26
  ping_count: int = 1
14
27
  timeout: float = 1.0
@@ -16,9 +29,24 @@ class PingConfig(BaseModel):
16
29
 
17
30
  @classmethod
18
31
  def from_dict(cls, data: dict) -> 'PingConfig':
32
+ """
33
+ Create a PingConfig instance from a dictionary.
34
+
35
+ Args:
36
+ data: Dictionary containing PingConfig parameters
37
+
38
+ Returns:
39
+ A new PingConfig instance with the provided settings
40
+ """
19
41
  return cls.model_validate(data)
20
42
 
21
43
  def to_dict(self) -> dict:
44
+ """
45
+ Convert the PingConfig instance to a dictionary.
46
+
47
+ Returns:
48
+ Dictionary representation of the PingConfig
49
+ """
22
50
  return self.model_dump()
23
51
 
24
52
  def __str__(self):
@@ -39,64 +67,186 @@ class ArpConfig(BaseModel):
39
67
 
40
68
  @classmethod
41
69
  def from_dict(cls, data: dict) -> 'ArpConfig':
70
+ """
71
+ Create an ArpConfig instance from a dictionary.
72
+
73
+ Args:
74
+ data: Dictionary containing ArpConfig parameters
75
+
76
+ Returns:
77
+ A new ArpConfig instance with the provided settings
78
+ """
42
79
  return cls.model_validate(data)
43
80
 
44
81
  def to_dict(self) -> dict:
82
+ """
83
+ Convert the ArpConfig instance to a dictionary.
84
+
85
+ Returns:
86
+ Dictionary representation of the ArpConfig
87
+ """
45
88
  return self.model_dump()
46
89
 
47
90
  def __str__(self):
48
91
  return f'ArpCfg(timeout={self.timeout}, attempts={self.attempts})'
49
92
 
50
93
 
94
+ class ArpCacheConfig(BaseModel):
95
+ """Config for fetching from ARP cache"""
96
+ attempts: int = 1
97
+ wait_before: float = 0.2
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: dict) -> 'ArpCacheConfig':
101
+ """
102
+ Create an ArpCacheConfig instance from a dictionary.
103
+
104
+ Args:
105
+ data: Dictionary containing ArpCacheConfig parameters
106
+
107
+ Returns:
108
+ A new ArpCacheConfig instance with the provided settings
109
+ """
110
+ return cls.model_validate(data)
111
+
112
+ def to_dict(self) -> dict:
113
+ """
114
+ Convert the ArpCacheConfig instance to a dictionary.
115
+
116
+ Returns:
117
+ Dictionary representation of the ArpCacheConfig
118
+ """
119
+ return self.model_dump()
120
+
121
+ def __str__(self):
122
+ return f'ArpCacheCfg(wait_before={self.wait_before}, attempts={self.attempts})'
123
+
124
+
125
+ class PokeConfig(BaseModel):
126
+ """
127
+ Poking essentially involves sending a TCP packet to a specific port on a device
128
+ to elicit a response. Not so much expecting a response, but it should at least
129
+ trigger an ARP request.
130
+ """
131
+ attempts: int = 1
132
+ timeout: float = 2.0
133
+
134
+ @classmethod
135
+ def from_dict(cls, data: dict) -> 'PokeConfig':
136
+ """
137
+ Create a PokeConfig instance from a dictionary.
138
+
139
+ Args:
140
+ data: Dictionary containing PokeConfig parameters
141
+
142
+ Returns:
143
+ A new PokeConfig instance with the provided settings
144
+ """
145
+ return cls.model_validate(data)
146
+
147
+ def to_dict(self) -> dict:
148
+ """
149
+ Convert the PokeConfig instance to a dictionary.
150
+
151
+ Returns:
152
+ Dictionary representation of the PokeConfig
153
+ """
154
+ return self.model_dump()
155
+
156
+
51
157
  class ScanType(Enum):
52
- PING = 'ping'
53
- ARP = 'arp'
54
- BOTH = 'both'
158
+ """
159
+ Enumeration of supported network scan types.
160
+
161
+ PING: Uses ICMP echo requests to determine if hosts are alive
162
+ ARP: Uses Address Resolution Protocol to discover hosts on the local network
163
+
164
+ """
165
+ ICMP = 'ICMP'
166
+ ARP_LOOKUP = 'ARP_LOOKUP'
167
+ POKE_THEN_ARP = 'POKE_THEN_ARP'
168
+ ICMP_THEN_ARP = 'ICMP_THEN_ARP'
55
169
 
56
170
 
57
171
  class ScanConfig(BaseModel):
172
+ """
173
+ Main configuration class for network scanning operations.
174
+
175
+ Contains settings for subnet targets, port ranges, thread counts,
176
+ scan tasks to perform, and configurations for different scan methods.
177
+ """
58
178
  subnet: str
59
179
  port_list: str
60
180
  t_multiplier: float = 1.0
61
- t_cnt_port_scan: int = 10
62
- t_cnt_port_test: int = 128
63
- t_cnt_isalive: int = 256
181
+ t_cnt_port_scan: int = os.cpu_count()
182
+ t_cnt_port_test: int = os.cpu_count() * 4
183
+ t_cnt_isalive: int = os.cpu_count() * 6
64
184
 
65
185
  task_scan_ports: bool = True
66
186
  # below wont run if above false
67
187
  task_scan_port_services: bool = False # disabling until more stable
68
188
 
69
- lookup_type: ScanType = ScanType.BOTH
189
+ lookup_type: List[ScanType] = [ScanType.ICMP_THEN_ARP]
70
190
 
71
191
  ping_config: PingConfig = Field(default_factory=PingConfig)
72
192
  arp_config: ArpConfig = Field(default_factory=ArpConfig)
193
+ poke_config: PokeConfig = Field(default_factory=PokeConfig)
194
+ arp_cache_config: ArpCacheConfig = Field(default_factory=ArpCacheConfig)
195
+
196
+ def t_cnt(self, thread_id: str) -> int:
197
+ """
198
+ Calculate thread count for a specific operation based on multiplier.
73
199
 
74
- def t_cnt(self, id: str) -> int:
75
- return int(int(getattr(self, f't_cnt_{id}')) * float(self.t_multiplier))
200
+ Args:
201
+ thread_id: String identifier for the thread type (e.g., 'port_scan')
202
+
203
+ Returns:
204
+ Calculated thread count for the specified operation
205
+ """
206
+ return int(int(getattr(self, f't_cnt_{thread_id}')) * float(self.t_multiplier))
76
207
 
77
208
  @classmethod
78
209
  def from_dict(cls, data: dict) -> 'ScanConfig':
79
- # Handle special cases before validation
80
- if isinstance(data.get('lookup_type'), str):
81
- data['lookup_type'] = ScanType[data['lookup_type'].upper()]
210
+ """
211
+ Create a ScanConfig instance from a dictionary.
212
+
213
+ Args:
214
+ data: Dictionary containing ScanConfig parameters
215
+
216
+ Returns:
217
+ A new ScanConfig instance with the provided settings
218
+ """
82
219
 
83
220
  return cls.model_validate(data)
84
221
 
85
222
  def to_dict(self) -> dict:
86
- dump = self.model_dump()
87
- dump['lookup_type'] = self.lookup_type.value
88
- return dump
223
+ """
224
+ Convert the ScanConfig instance to a json-serializable dictionary.
225
+ """
226
+ return self.model_dump(mode="json")
89
227
 
90
228
  def get_ports(self) -> List[int]:
229
+ """
230
+ Get the list of ports to scan based on the configured port list name.
231
+
232
+ Returns:
233
+ List of port numbers to scan
234
+ """
91
235
  return PortManager().get_port_list(self.port_list).keys()
92
236
 
93
237
  def parse_subnet(self) -> List[ipaddress.IPv4Network]:
238
+ """
239
+ Parse the configured subnet string into IPv4Network objects.
240
+
241
+ Returns:
242
+ List of IPv4Network objects representing the target networks
243
+ """
94
244
  return parse_ip_input(self.subnet)
95
245
 
96
246
  def __str__(self):
97
247
  a = f'subnet={self.subnet}'
98
248
  b = f'ports={self.port_list}'
99
- c = f'scan_type={self.lookup_type.value}'
249
+ c = f'scan_type={[st.value for st in self.lookup_type]}'
100
250
  return f'ScanConfig({a}, {b}, {c})'
101
251
 
102
252
 
@@ -110,7 +260,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
110
260
  t_cnt_isalive=64,
111
261
  task_scan_ports=True,
112
262
  task_scan_port_services=False,
113
- lookup_type=ScanType.BOTH,
263
+ lookup_type=[ScanType.ICMP_THEN_ARP, ScanType.ARP_LOOKUP],
114
264
  arp_config=ArpConfig(
115
265
  attempts=3,
116
266
  timeout=2.5
@@ -120,6 +270,10 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
120
270
  ping_count=2,
121
271
  timeout=1.5,
122
272
  retry_delay=0.5
273
+ ),
274
+ arp_cache_config=ArpCacheConfig(
275
+ attempts=2,
276
+ wait_before=0.3
123
277
  )
124
278
  ),
125
279
  'fast': ScanConfig(
@@ -130,7 +284,7 @@ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
130
284
  t_cnt_isalive=512,
131
285
  task_scan_ports=True,
132
286
  task_scan_port_services=False,
133
- lookup_type=ScanType.BOTH,
287
+ lookup_type=[ScanType.POKE_THEN_ARP],
134
288
  arp_config=ArpConfig(
135
289
  attempts=1,
136
290
  timeout=1.0
@@ -1,7 +1,8 @@
1
+ """Service scanning module for identifying services running on network ports."""
1
2
  import asyncio
2
3
  import logging
3
4
  import traceback
4
- from .app_scope import ResourceManager
5
+ from lanscape.libraries.app_scope import ResourceManager
5
6
 
6
7
  log = logging.getLogger('ServiceScan')
7
8
  SERVICES = ResourceManager('services').get_jsonc('definitions.jsonc')
@@ -24,8 +25,7 @@ def scan_service(ip: str, port: int, timeout=10) -> str:
24
25
  reader, writer = await asyncio.wait_for(asyncio.open_connection(ip, port), timeout=5)
25
26
 
26
27
  # Send a probe appropriate for common services
27
- probe = "GET / HTTP/1.1\r\nHost: {}\r\n\r\n".format(
28
- ip).encode("utf-8")
28
+ probe = f"GET / HTTP/1.1\r\nHost: {ip}\r\n\r\n".encode("utf-8")
29
29
  writer.write(probe)
30
30
  await writer.drain()
31
31
 
@@ -1,5 +1,10 @@
1
- from .scan_config import ScanConfig
2
- from .decorators import job_tracker, terminator, JobStatsMixin
1
+ """
2
+ Network subnet scanning module for LANscape.
3
+ Provides classes for performing network discovery, device scanning, and port scanning.
4
+ Handles scan management, result tracking, and scan termination.
5
+ """
6
+
7
+ # Standard library imports
3
8
  import os
4
9
  import json
5
10
  import uuid
@@ -7,33 +12,46 @@ import logging
7
12
  import ipaddress
8
13
  import traceback
9
14
  import threading
10
- from time import time
11
- from time import sleep
15
+ from time import time, sleep
12
16
  from typing import List, Union
13
- from tabulate import tabulate
14
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
15
18
 
16
- from .net_tools import Device, is_arp_supported
17
- from .errors import SubnetScanTerminationFailure
19
+ # Third-party imports
20
+ from tabulate import tabulate
21
+
22
+ # Local imports
23
+ from lanscape.libraries.scan_config import ScanConfig
24
+ from lanscape.libraries.decorators import job_tracker, terminator, JobStats
25
+ from lanscape.libraries.net_tools import Device
26
+ from lanscape.libraries.errors import SubnetScanTerminationFailure
27
+ from lanscape.libraries.device_alive import is_device_alive
28
+
18
29
 
30
+ class SubnetScanner():
31
+ """
32
+ Scans a subnet for devices and open ports.
33
+
34
+ Manages the scanning process including device discovery and port scanning.
35
+ Tracks scan progress and provides mechanisms for controlled termination.
36
+ """
19
37
 
20
- class SubnetScanner(JobStatsMixin):
21
38
  def __init__(
22
39
  self,
23
40
  config: ScanConfig
24
41
  ):
42
+ # Config and network properties
25
43
  self.cfg = config
26
44
  self.subnet = config.parse_subnet()
27
45
  self.ports: List[int] = config.get_ports()
28
- self.running = False
29
46
  self.subnet_str = config.subnet
47
+ self.job_stats = JobStats()
30
48
 
49
+ # Status properties
50
+ self.running = False
31
51
  self.uid = str(uuid.uuid4())
32
52
  self.results = ScannerResults(self)
33
53
  self.log: logging.Logger = logging.getLogger('SubnetScanner')
34
- if not is_arp_supported():
35
- self.log.warning(
36
- 'ARP is not supported with the active runtime context. Device discovery will be limited to ping responses.')
54
+
37
55
  self.log.debug(f'Instantiated with uid: {self.uid}')
38
56
  self.log.debug(
39
57
  f'Port Count: {len(self.ports)} | Device Count: {len(self.subnet)}')
@@ -68,16 +86,36 @@ class SubnetScanner(JobStatsMixin):
68
86
  return self.results
69
87
 
70
88
  def terminate(self):
89
+ """
90
+ Terminate the scan operation.
91
+
92
+ Attempts a graceful shutdown of all scan operations and waits for running
93
+ tasks to complete. Raises an exception if termination takes too long.
94
+
95
+ Returns:
96
+ bool: True if terminated successfully
97
+
98
+ Raises:
99
+ SubnetScanTerminationFailure: If the scan cannot be terminated within the timeout
100
+ """
71
101
  self.running = False
72
102
  self._set_stage('terminating')
73
- for i in range(20):
74
- if not len(self.job_stats.running.keys()):
103
+ for _ in range(20):
104
+ if not self.job_stats.running:
75
105
  self._set_stage('terminated')
76
106
  return True
77
107
  sleep(.5)
78
108
  raise SubnetScanTerminationFailure(self.job_stats.running)
79
109
 
80
110
  def calc_percent_complete(self) -> int: # 0 - 100
111
+ """
112
+ Calculate the percentage completion of the scan.
113
+
114
+ Uses scan statistics and job timing information to estimate progress.
115
+
116
+ Returns:
117
+ int: Completion percentage (0-100)
118
+ """
81
119
  if not self.running:
82
120
  return 100
83
121
 
@@ -85,7 +123,7 @@ class SubnetScanner(JobStatsMixin):
85
123
  avg_host_detail_sec = self.job_stats.timing.get(
86
124
  '_get_host_details', 4.5)
87
125
  # assume 10% alive percentage if the scan just started
88
- if len(self.results.devices) and (self.results.devices_scanned):
126
+ if self.results.devices and self.results.devices_scanned:
89
127
  est_subnet_alive_percent = (
90
128
  # avoid div 0
91
129
  len(self.results.devices)) / (self.results.devices_scanned)
@@ -134,6 +172,7 @@ class SubnetScanner(JobStatsMixin):
134
172
  t_remain = int((100 - percent) * (t_elapsed / percent)
135
173
  ) if percent else '∞'
136
174
  buffer = f'{self.uid} - {self.subnet_str}\n'
175
+ buffer += f'Config: {self.cfg}\n'
137
176
  buffer += f'Elapsed: {int(t_elapsed)} sec - Remain: {t_remain} sec\n'
138
177
  buffer += f'Scanned: {self.results.devices_scanned}/{self.results.devices_total}'
139
178
  buffer += f' - {percent}%\n'
@@ -198,12 +237,7 @@ class SubnetScanner(JobStatsMixin):
198
237
  """
199
238
  Ping the given host and return True if it's reachable, False otherwise.
200
239
  """
201
- return host.is_alive(
202
- host.ip,
203
- scan_type=self.cfg.lookup_type,
204
- ping_config=self.cfg.ping_config,
205
- arp_config=self.cfg.arp_config
206
- )
240
+ return is_device_alive(host, self.cfg)
207
241
 
208
242
  def _set_stage(self, stage):
209
243
  self.log.debug(f'[{self.uid}] Moving to Stage: {stage}')
@@ -213,41 +247,71 @@ class SubnetScanner(JobStatsMixin):
213
247
 
214
248
 
215
249
  class ScannerResults:
250
+ """
251
+ Stores and manages the results of a subnet scan.
252
+
253
+ Tracks devices found, scan statistics, and provides export functionality
254
+ for scan results. Also handles runtime calculation and progress tracking.
255
+ """
256
+
216
257
  def __init__(self, scan: SubnetScanner):
258
+ # Parent reference and identifiers
217
259
  self.scan = scan
218
260
  self.port_list: str = scan.cfg.port_list
219
261
  self.subnet: str = scan.subnet_str
220
262
  self.uid = scan.uid
221
263
 
264
+ # Scan statistics
222
265
  self.devices_total: int = len(list(scan.subnet))
223
266
  self.devices_scanned: int = 0
224
267
  self.devices: List[Device] = []
225
268
 
269
+ # Status tracking
226
270
  self.errors: List[str] = []
227
271
  self.running: bool = False
228
272
  self.start_time: float = time()
229
273
  self.end_time: int = None
230
274
  self.stage = 'instantiated'
275
+ self.run_time = 0
231
276
 
277
+ # Logging
232
278
  self.log = logging.getLogger('ScannerResults')
233
279
  self.log.debug(f'Instantiated Logger For Scan: {self.scan.uid}')
234
280
 
281
+ @property
282
+ def devices_alive(self):
283
+ """number of alive devices found in the scan"""
284
+ return len(self.devices)
285
+
235
286
  def scanned(self):
287
+ """
288
+ Increment the count of scanned devices.
289
+ """
236
290
  self.devices_scanned += 1
237
291
 
238
292
  def get_runtime(self):
293
+ """
294
+ Calculate the runtime of the scan in seconds.
295
+
296
+ Returns:
297
+ int: Runtime in seconds
298
+ """
239
299
  if self.scan.running:
240
300
  return int(time() - self.start_time)
241
301
  return int(self.end_time - self.start_time)
242
302
 
243
303
  def export(self, out_type=dict) -> Union[str, dict]:
244
304
  """
245
- Returns json representation of the scan
246
- """
305
+ Export scan results in the specified format.
247
306
 
307
+ Args:
308
+ out_type: The output type (dict or str)
309
+
310
+ Returns:
311
+ Union[str, dict]: Scan results in the specified format
312
+ """
248
313
  self.running = self.scan.running
249
314
  self.run_time = int(round(time() - self.start_time, 0))
250
- self.devices_alive = len(self.devices)
251
315
 
252
316
  out = vars(self).copy()
253
317
  out.pop('scan')
@@ -255,9 +319,9 @@ class ScannerResults:
255
319
  out['cfg'] = vars(self.scan.cfg)
256
320
 
257
321
  devices: List[Device] = out.pop('devices')
258
- sortedDevices = sorted(
322
+ sorted_devices = sorted(
259
323
  devices, key=lambda obj: ipaddress.IPv4Address(obj.ip))
260
- out['devices'] = [device.dict() for device in sortedDevices]
324
+ out['devices'] = [device.dict() for device in sorted_devices]
261
325
 
262
326
  if out_type == str:
263
327
  return json.dumps(out, default=str, indent=2)
@@ -280,6 +344,7 @@ class ScannerResults:
280
344
 
281
345
  # Format and return the complete buffer with table output
282
346
  buffer = f"Scan Results - {self.scan.subnet_str} - {self.uid}\n"
347
+ buffer += f'Found/Scanned: {self.devices_alive}/{self.devices_scanned}\n'
283
348
  buffer += "---------------------------------------------\n\n"
284
349
  buffer += table
285
350
  return buffer
@@ -304,6 +369,15 @@ class ScanManager:
304
369
  self.log = logging.getLogger('ScanManager')
305
370
 
306
371
  def new_scan(self, config: ScanConfig) -> SubnetScanner:
372
+ """
373
+ Create and start a new scan with the given configuration.
374
+
375
+ Args:
376
+ config: The scan configuration
377
+
378
+ Returns:
379
+ SubnetScanner: The newly created scan instance
380
+ """
307
381
  scan = SubnetScanner(config)
308
382
  self._start(scan)
309
383
  self.log.info(f'Scan started - {config}')
@@ -317,6 +391,7 @@ class ScanManager:
317
391
  for scan in self.scans:
318
392
  if scan.uid == scan_id:
319
393
  return scan
394
+ return None # Explicitly return None for consistency
320
395
 
321
396
  def terminate_scans(self):
322
397
  """
@@ -327,12 +402,22 @@ class ScanManager:
327
402
  scan.terminate()
328
403
 
329
404
  def wait_until_complete(self, scan_id: str) -> SubnetScanner:
405
+ """Wait for a scan to complete."""
330
406
  scan = self.get_scan(scan_id)
331
407
  while scan.running:
332
408
  sleep(.5)
333
409
  return scan
334
410
 
335
411
  def _start(self, scan: SubnetScanner):
412
+ """
413
+ Start a scan in a separate thread.
414
+
415
+ Args:
416
+ scan: The scan to start
417
+
418
+ Returns:
419
+ Thread: The thread running the scan
420
+ """
336
421
  t = threading.Thread(target=scan.start)
337
422
  t.start()
338
423
  return t
@@ -1,9 +1,16 @@
1
+ """
2
+ Version management module for LANscape.
3
+ Handles version checking, update detection, and retrieving package information
4
+ from both local installation and PyPI repository.
5
+ """
6
+
1
7
  import logging
2
- import requests
3
8
  import traceback
4
9
  from importlib.metadata import version, PackageNotFoundError
5
10
  from random import randint
6
11
 
12
+ import requests
13
+
7
14
  from .app_scope import is_local_run
8
15
 
9
16
  log = logging.getLogger('VersionManager')
@@ -11,10 +18,23 @@ log = logging.getLogger('VersionManager')
11
18
  PACKAGE = 'lanscape'
12
19
  LOCAL_VERSION = '0.0.0'
13
20
 
14
- latest = None # used to 'remember' pypi version each runtime
21
+ # Used to cache PyPI version during runtime
22
+ LATEST_VERSION = None
15
23
 
16
24
 
17
25
  def is_update_available(package=PACKAGE) -> bool:
26
+ """
27
+ Check if an update is available for the package.
28
+
29
+ Compares the installed version with the latest version available on PyPI.
30
+ Ignores pre-release versions (alpha/beta) and local development installs.
31
+
32
+ Args:
33
+ package: The package name to check for updates
34
+
35
+ Returns:
36
+ Boolean indicating if an update is available
37
+ """
18
38
  installed = get_installed_version(package)
19
39
  available = lookup_latest_version(package)
20
40
 
@@ -30,23 +50,46 @@ def is_update_available(package=PACKAGE) -> bool:
30
50
 
31
51
 
32
52
  def lookup_latest_version(package=PACKAGE):
53
+ """
54
+ Retrieve the latest version of the package from PyPI.
55
+
56
+ Caches the result for subsequent calls during the same runtime.
57
+
58
+ Args:
59
+ package: The package name to lookup
60
+
61
+ Returns:
62
+ The latest version string from PyPI or None if retrieval fails
63
+ """
33
64
  # Fetch the latest version from PyPI
34
- global latest
35
- if not latest:
65
+ global LATEST_VERSION # pylint: disable=global-statement
66
+ if not LATEST_VERSION:
36
67
  no_cache = f'?cachebust={randint(0, 6969)}'
37
68
  url = f"https://pypi.org/pypi/{package}/json{no_cache}"
38
69
  try:
39
70
  response = requests.get(url, timeout=5)
40
71
  response.raise_for_status() # Raise an exception for HTTP errors
41
- latest = response.json()['info']['version']
42
- log.debug(f'Latest pypi version: {latest}')
72
+ LATEST_VERSION = response.json()['info']['version']
73
+ log.debug(f'Latest pypi version: {LATEST_VERSION}')
43
74
  except BaseException:
44
75
  log.debug(traceback.format_exc())
45
76
  log.warning('Unable to fetch package version from PyPi')
46
- return latest
77
+ return LATEST_VERSION
47
78
 
48
79
 
49
80
  def get_installed_version(package=PACKAGE):
81
+ """
82
+ Get the installed version of the package.
83
+
84
+ Returns the current installed version or a default local version
85
+ if running in development mode or if the package is not found.
86
+
87
+ Args:
88
+ package: The package name to check
89
+
90
+ Returns:
91
+ The installed version string or LOCAL_VERSION for local development
92
+ """
50
93
  if not is_local_run():
51
94
  try:
52
95
  return version(package)