lanscape 1.3.4__py3-none-any.whl → 1.3.5a2__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.
Files changed (39) hide show
  1. lanscape/libraries/app_scope.py +0 -1
  2. lanscape/libraries/decorators.py +10 -5
  3. lanscape/libraries/errors.py +10 -0
  4. lanscape/libraries/ip_parser.py +73 -1
  5. lanscape/libraries/logger.py +29 -1
  6. lanscape/libraries/mac_lookup.py +5 -0
  7. lanscape/libraries/net_tools.py +139 -68
  8. lanscape/libraries/port_manager.py +83 -0
  9. lanscape/libraries/runtime_args.py +12 -0
  10. lanscape/libraries/scan_config.py +148 -5
  11. lanscape/libraries/service_scan.py +3 -3
  12. lanscape/libraries/subnet_scan.py +104 -16
  13. lanscape/libraries/version_manager.py +50 -7
  14. lanscape/libraries/web_browser.py +136 -68
  15. lanscape/resources/mac_addresses/convert_csv.py +13 -2
  16. lanscape/resources/ports/convert_csv.py +13 -3
  17. lanscape/ui/__init__.py +0 -0
  18. lanscape/ui/app.py +32 -36
  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 +58 -10
  23. lanscape/ui/blueprints/api/tools.py +17 -1
  24. lanscape/ui/blueprints/web/__init__.py +4 -0
  25. lanscape/ui/blueprints/web/routes.py +52 -5
  26. lanscape/ui/main.py +17 -7
  27. lanscape/ui/shutdown_handler.py +57 -0
  28. lanscape/ui/static/css/style.css +94 -20
  29. lanscape/ui/static/js/main.js +25 -48
  30. lanscape/ui/static/js/scan-config.js +107 -0
  31. lanscape/ui/static/lanscape.webmanifest +4 -3
  32. lanscape/ui/templates/main.html +39 -36
  33. lanscape/ui/templates/scan/config.html +168 -0
  34. {lanscape-1.3.4.dist-info → lanscape-1.3.5a2.dist-info}/METADATA +1 -1
  35. lanscape-1.3.5a2.dist-info/RECORD +73 -0
  36. lanscape-1.3.4.dist-info/RECORD +0 -69
  37. {lanscape-1.3.4.dist-info → lanscape-1.3.5a2.dist-info}/WHEEL +0 -0
  38. {lanscape-1.3.4.dist-info → lanscape-1.3.5a2.dist-info}/licenses/LICENSE +0 -0
  39. {lanscape-1.3.4.dist-info → lanscape-1.3.5a2.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ """Runtime argument handler for LANscape as module"""
2
+
1
3
  from dataclasses import dataclass, fields
2
4
  import argparse
3
5
  from typing import Any, Dict, Optional
@@ -5,6 +7,7 @@ from typing import Any, Dict, Optional
5
7
 
6
8
  @dataclass
7
9
  class RuntimeArgs:
10
+ """Class representing runtime arguments for the application."""
8
11
  reloader: bool = False
9
12
  port: int = 5001
10
13
  logfile: Optional[str] = None
@@ -14,6 +17,9 @@ class RuntimeArgs:
14
17
 
15
18
 
16
19
  def parse_args() -> RuntimeArgs:
20
+ """
21
+ Parse command line arguments and return a RuntimeArgs instance.
22
+ """
17
23
  parser = argparse.ArgumentParser(description='LANscape')
18
24
 
19
25
  parser.add_argument('--reloader', action='store_true',
@@ -27,6 +33,8 @@ def parse_args() -> RuntimeArgs:
27
33
  help='Enable flask logging (disables click output)')
28
34
  parser.add_argument('--persistent', action='store_true',
29
35
  help='Don\'t exit after browser is closed')
36
+ parser.add_argument('--debug', action='store_true',
37
+ help='Shorthand debug mode (equivalent to "--loglevel DEBUG --reloader")')
30
38
 
31
39
  # Parse the arguments
32
40
  args = parser.parse_args()
@@ -37,6 +45,10 @@ def parse_args() -> RuntimeArgs:
37
45
  field_names = {field.name for field in fields(
38
46
  RuntimeArgs)} # Get dataclass field names
39
47
 
48
+ if args.debug:
49
+ args_dict['loglevel'] = 'DEBUG'
50
+ args_dict['reloader'] = True
51
+
40
52
  # Only pass arguments that exist in the Args dataclass
41
53
  filtered_args = {name: args_dict[name]
42
54
  for name in field_names if name in args_dict}
@@ -1,13 +1,26 @@
1
- from typing import List
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
+ from typing import List, Dict
2
8
  import ipaddress
3
- from pydantic import BaseModel, Field
4
9
  from enum import Enum
5
10
 
11
+ from pydantic import BaseModel, Field
12
+
6
13
  from lanscape.libraries.port_manager import PortManager
7
14
  from lanscape.libraries.ip_parser import parse_ip_input
8
15
 
9
16
 
10
17
  class PingConfig(BaseModel):
18
+ """
19
+ Configuration settings for ICMP ping-based network scanning.
20
+
21
+ Controls parameters such as the number of ping attempts, count per ping,
22
+ timeout values, and retry delays to optimize ping scanning behavior.
23
+ """
11
24
  attempts: int = 2
12
25
  ping_count: int = 1
13
26
  timeout: float = 1.0
@@ -15,9 +28,24 @@ class PingConfig(BaseModel):
15
28
 
16
29
  @classmethod
17
30
  def from_dict(cls, data: dict) -> 'PingConfig':
31
+ """
32
+ Create a PingConfig instance from a dictionary.
33
+
34
+ Args:
35
+ data: Dictionary containing PingConfig parameters
36
+
37
+ Returns:
38
+ A new PingConfig instance with the provided settings
39
+ """
18
40
  return cls.model_validate(data)
19
41
 
20
42
  def to_dict(self) -> dict:
43
+ """
44
+ Convert the PingConfig instance to a dictionary.
45
+
46
+ Returns:
47
+ Dictionary representation of the PingConfig
48
+ """
21
49
  return self.model_dump()
22
50
 
23
51
  def __str__(self):
@@ -38,9 +66,24 @@ class ArpConfig(BaseModel):
38
66
 
39
67
  @classmethod
40
68
  def from_dict(cls, data: dict) -> 'ArpConfig':
69
+ """
70
+ Create an ArpConfig instance from a dictionary.
71
+
72
+ Args:
73
+ data: Dictionary containing ArpConfig parameters
74
+
75
+ Returns:
76
+ A new ArpConfig instance with the provided settings
77
+ """
41
78
  return cls.model_validate(data)
42
79
 
43
80
  def to_dict(self) -> dict:
81
+ """
82
+ Convert the ArpConfig instance to a dictionary.
83
+
84
+ Returns:
85
+ Dictionary representation of the ArpConfig
86
+ """
44
87
  return self.model_dump()
45
88
 
46
89
  def __str__(self):
@@ -48,12 +91,25 @@ class ArpConfig(BaseModel):
48
91
 
49
92
 
50
93
  class ScanType(Enum):
94
+ """
95
+ Enumeration of supported network scan types.
96
+
97
+ PING: Uses ICMP echo requests to determine if hosts are alive
98
+ ARP: Uses Address Resolution Protocol to discover hosts on the local network
99
+ BOTH: Uses both PING and ARP methods for maximum coverage
100
+ """
51
101
  PING = 'ping'
52
102
  ARP = 'arp'
53
103
  BOTH = 'both'
54
104
 
55
105
 
56
106
  class ScanConfig(BaseModel):
107
+ """
108
+ Main configuration class for network scanning operations.
109
+
110
+ Contains settings for subnet targets, port ranges, thread counts,
111
+ scan tasks to perform, and configurations for different scan methods.
112
+ """
57
113
  subnet: str
58
114
  port_list: str
59
115
  t_multiplier: float = 1.0
@@ -70,11 +126,31 @@ class ScanConfig(BaseModel):
70
126
  ping_config: PingConfig = Field(default_factory=PingConfig)
71
127
  arp_config: ArpConfig = Field(default_factory=ArpConfig)
72
128
 
73
- def t_cnt(self, id: str) -> int:
74
- return int(int(getattr(self, f't_cnt_{id}')) * float(self.t_multiplier))
129
+ def t_cnt(self, thread_id: str) -> int:
130
+ """
131
+ Calculate thread count for a specific operation based on multiplier.
132
+
133
+ Args:
134
+ thread_id: String identifier for the thread type (e.g., 'port_scan')
135
+
136
+ Returns:
137
+ Calculated thread count for the specified operation
138
+ """
139
+ return int(int(getattr(self, f't_cnt_{thread_id}')) * float(self.t_multiplier))
75
140
 
76
141
  @classmethod
77
142
  def from_dict(cls, data: dict) -> 'ScanConfig':
143
+ """
144
+ Create a ScanConfig instance from a dictionary.
145
+
146
+ Handles special cases like converting string enum values to proper Enum types.
147
+
148
+ Args:
149
+ data: Dictionary containing ScanConfig parameters
150
+
151
+ Returns:
152
+ A new ScanConfig instance with the provided settings
153
+ """
78
154
  # Handle special cases before validation
79
155
  if isinstance(data.get('lookup_type'), str):
80
156
  data['lookup_type'] = ScanType[data['lookup_type'].upper()]
@@ -82,12 +158,34 @@ class ScanConfig(BaseModel):
82
158
  return cls.model_validate(data)
83
159
 
84
160
  def to_dict(self) -> dict:
85
- return self.model_dump()
161
+ """
162
+ Convert the ScanConfig instance to a dictionary.
163
+
164
+ Handles special cases like converting Enum values to strings.
165
+
166
+ Returns:
167
+ Dictionary representation of the ScanConfig
168
+ """
169
+ dump = self.model_dump()
170
+ dump['lookup_type'] = self.lookup_type.value
171
+ return dump
86
172
 
87
173
  def get_ports(self) -> List[int]:
174
+ """
175
+ Get the list of ports to scan based on the configured port list name.
176
+
177
+ Returns:
178
+ List of port numbers to scan
179
+ """
88
180
  return PortManager().get_port_list(self.port_list).keys()
89
181
 
90
182
  def parse_subnet(self) -> List[ipaddress.IPv4Network]:
183
+ """
184
+ Parse the configured subnet string into IPv4Network objects.
185
+
186
+ Returns:
187
+ List of IPv4Network objects representing the target networks
188
+ """
91
189
  return parse_ip_input(self.subnet)
92
190
 
93
191
  def __str__(self):
@@ -95,3 +193,48 @@ class ScanConfig(BaseModel):
95
193
  b = f'ports={self.port_list}'
96
194
  c = f'scan_type={self.lookup_type.value}'
97
195
  return f'ScanConfig({a}, {b}, {c})'
196
+
197
+
198
+ DEFAULT_CONFIGS: Dict[str, ScanConfig] = {
199
+ 'balanced': ScanConfig(subnet='', port_list='medium'),
200
+ 'accurate': ScanConfig(
201
+ subnet='',
202
+ port_list='large',
203
+ t_cnt_port_scan=5,
204
+ t_cnt_port_test=64,
205
+ t_cnt_isalive=64,
206
+ task_scan_ports=True,
207
+ task_scan_port_services=False,
208
+ lookup_type=ScanType.BOTH,
209
+ arp_config=ArpConfig(
210
+ attempts=3,
211
+ timeout=2.5
212
+ ),
213
+ ping_config=PingConfig(
214
+ attempts=3,
215
+ ping_count=2,
216
+ timeout=1.5,
217
+ retry_delay=0.5
218
+ )
219
+ ),
220
+ 'fast': ScanConfig(
221
+ subnet='',
222
+ port_list='small',
223
+ t_cnt_port_scan=20,
224
+ t_cnt_port_test=256,
225
+ t_cnt_isalive=512,
226
+ task_scan_ports=True,
227
+ task_scan_port_services=False,
228
+ lookup_type=ScanType.BOTH,
229
+ arp_config=ArpConfig(
230
+ attempts=1,
231
+ timeout=1.0
232
+ ),
233
+ ping_config=PingConfig(
234
+ attempts=1,
235
+ ping_count=1,
236
+ timeout=0.5,
237
+ retry_delay=0.25
238
+ )
239
+ )
240
+ }
@@ -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,49 @@ 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, JobStatsMixin
25
+ from lanscape.libraries.net_tools import Device, is_arp_supported
26
+ from lanscape.libraries.errors import SubnetScanTerminationFailure
18
27
 
19
28
 
20
29
  class SubnetScanner(JobStatsMixin):
30
+ """
31
+ Scans a subnet for devices and open ports.
32
+
33
+ Manages the scanning process including device discovery and port scanning.
34
+ Tracks scan progress and provides mechanisms for controlled termination.
35
+ """
36
+
21
37
  def __init__(
22
38
  self,
23
39
  config: ScanConfig
24
40
  ):
41
+ # Config and network properties
25
42
  self.cfg = config
26
43
  self.subnet = config.parse_subnet()
27
44
  self.ports: List[int] = config.get_ports()
28
- self.running = False
29
45
  self.subnet_str = config.subnet
30
46
 
47
+ # Status properties
48
+ self.running = False
31
49
  self.uid = str(uuid.uuid4())
32
50
  self.results = ScannerResults(self)
33
51
  self.log: logging.Logger = logging.getLogger('SubnetScanner')
52
+
53
+ # Initial logging
34
54
  if not is_arp_supported():
35
55
  self.log.warning(
36
- 'ARP is not supported with the active runtime context. Device discovery will be limited to ping responses.')
56
+ 'ARP is not supported with the active runtime context. '
57
+ 'Device discovery will be limited to ping responses.')
37
58
  self.log.debug(f'Instantiated with uid: {self.uid}')
38
59
  self.log.debug(
39
60
  f'Port Count: {len(self.ports)} | Device Count: {len(self.subnet)}')
@@ -68,16 +89,36 @@ class SubnetScanner(JobStatsMixin):
68
89
  return self.results
69
90
 
70
91
  def terminate(self):
92
+ """
93
+ Terminate the scan operation.
94
+
95
+ Attempts a graceful shutdown of all scan operations and waits for running
96
+ tasks to complete. Raises an exception if termination takes too long.
97
+
98
+ Returns:
99
+ bool: True if terminated successfully
100
+
101
+ Raises:
102
+ SubnetScanTerminationFailure: If the scan cannot be terminated within the timeout
103
+ """
71
104
  self.running = False
72
105
  self._set_stage('terminating')
73
- for i in range(20):
74
- if not len(self.job_stats.running.keys()):
106
+ for _ in range(20):
107
+ if not self.job_stats.running:
75
108
  self._set_stage('terminated')
76
109
  return True
77
110
  sleep(.5)
78
111
  raise SubnetScanTerminationFailure(self.job_stats.running)
79
112
 
80
113
  def calc_percent_complete(self) -> int: # 0 - 100
114
+ """
115
+ Calculate the percentage completion of the scan.
116
+
117
+ Uses scan statistics and job timing information to estimate progress.
118
+
119
+ Returns:
120
+ int: Completion percentage (0-100)
121
+ """
81
122
  if not self.running:
82
123
  return 100
83
124
 
@@ -85,7 +126,7 @@ class SubnetScanner(JobStatsMixin):
85
126
  avg_host_detail_sec = self.job_stats.timing.get(
86
127
  '_get_host_details', 4.5)
87
128
  # assume 10% alive percentage if the scan just started
88
- if len(self.results.devices) and (self.results.devices_scanned):
129
+ if self.results.devices and self.results.devices_scanned:
89
130
  est_subnet_alive_percent = (
90
131
  # avoid div 0
91
132
  len(self.results.devices)) / (self.results.devices_scanned)
@@ -213,38 +254,65 @@ class SubnetScanner(JobStatsMixin):
213
254
 
214
255
 
215
256
  class ScannerResults:
257
+ """
258
+ Stores and manages the results of a subnet scan.
259
+
260
+ Tracks devices found, scan statistics, and provides export functionality
261
+ for scan results. Also handles runtime calculation and progress tracking.
262
+ """
263
+
216
264
  def __init__(self, scan: SubnetScanner):
265
+ # Parent reference and identifiers
217
266
  self.scan = scan
218
267
  self.port_list: str = scan.cfg.port_list
219
268
  self.subnet: str = scan.subnet_str
220
269
  self.uid = scan.uid
221
270
 
271
+ # Scan statistics
222
272
  self.devices_total: int = len(list(scan.subnet))
223
273
  self.devices_scanned: int = 0
224
274
  self.devices: List[Device] = []
275
+ self.devices_alive = 0
225
276
 
277
+ # Status tracking
226
278
  self.errors: List[str] = []
227
279
  self.running: bool = False
228
280
  self.start_time: float = time()
229
281
  self.end_time: int = None
230
282
  self.stage = 'instantiated'
283
+ self.run_time = 0
231
284
 
285
+ # Logging
232
286
  self.log = logging.getLogger('ScannerResults')
233
287
  self.log.debug(f'Instantiated Logger For Scan: {self.scan.uid}')
234
288
 
235
289
  def scanned(self):
290
+ """
291
+ Increment the count of scanned devices.
292
+ """
236
293
  self.devices_scanned += 1
237
294
 
238
295
  def get_runtime(self):
296
+ """
297
+ Calculate the runtime of the scan in seconds.
298
+
299
+ Returns:
300
+ int: Runtime in seconds
301
+ """
239
302
  if self.scan.running:
240
303
  return int(time() - self.start_time)
241
304
  return int(self.end_time - self.start_time)
242
305
 
243
306
  def export(self, out_type=dict) -> Union[str, dict]:
244
307
  """
245
- Returns json representation of the scan
246
- """
308
+ Export scan results in the specified format.
247
309
 
310
+ Args:
311
+ out_type: The output type (dict or str)
312
+
313
+ Returns:
314
+ Union[str, dict]: Scan results in the specified format
315
+ """
248
316
  self.running = self.scan.running
249
317
  self.run_time = int(round(time() - self.start_time, 0))
250
318
  self.devices_alive = len(self.devices)
@@ -255,9 +323,9 @@ class ScannerResults:
255
323
  out['cfg'] = vars(self.scan.cfg)
256
324
 
257
325
  devices: List[Device] = out.pop('devices')
258
- sortedDevices = sorted(
326
+ sorted_devices = sorted(
259
327
  devices, key=lambda obj: ipaddress.IPv4Address(obj.ip))
260
- out['devices'] = [device.dict() for device in sortedDevices]
328
+ out['devices'] = [device.dict() for device in sorted_devices]
261
329
 
262
330
  if out_type == str:
263
331
  return json.dumps(out, default=str, indent=2)
@@ -304,6 +372,15 @@ class ScanManager:
304
372
  self.log = logging.getLogger('ScanManager')
305
373
 
306
374
  def new_scan(self, config: ScanConfig) -> SubnetScanner:
375
+ """
376
+ Create and start a new scan with the given configuration.
377
+
378
+ Args:
379
+ config: The scan configuration
380
+
381
+ Returns:
382
+ SubnetScanner: The newly created scan instance
383
+ """
307
384
  scan = SubnetScanner(config)
308
385
  self._start(scan)
309
386
  self.log.info(f'Scan started - {config}')
@@ -317,6 +394,7 @@ class ScanManager:
317
394
  for scan in self.scans:
318
395
  if scan.uid == scan_id:
319
396
  return scan
397
+ return None # Explicitly return None for consistency
320
398
 
321
399
  def terminate_scans(self):
322
400
  """
@@ -327,12 +405,22 @@ class ScanManager:
327
405
  scan.terminate()
328
406
 
329
407
  def wait_until_complete(self, scan_id: str) -> SubnetScanner:
408
+ """Wait for a scan to complete."""
330
409
  scan = self.get_scan(scan_id)
331
410
  while scan.running:
332
411
  sleep(.5)
333
412
  return scan
334
413
 
335
414
  def _start(self, scan: SubnetScanner):
415
+ """
416
+ Start a scan in a separate thread.
417
+
418
+ Args:
419
+ scan: The scan to start
420
+
421
+ Returns:
422
+ Thread: The thread running the scan
423
+ """
336
424
  t = threading.Thread(target=scan.start)
337
425
  t.start()
338
426
  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)