import difflib
import importlib.util as ut
import os
import re
import time
from datetime import datetime, timezone, timedelta
from typing import Optional, Dict, Any
[docs]
class TS:
"""Timestamp handling utility class with automatic timezone detection."""
[docs]
def __init__(self, time_zone: Optional[str] = None, utc_offset: Optional[float] = None):
"""
Initialize timestamp handler with automatic local timezone detection.
Args:
time_zone: Timezone name (optional, auto-detects if None)
utc_offset: UTC offset in hours (optional, auto-detects if None)
"""
if utc_offset is not None:
# Explicit UTC offset provided
self.utc_offset = utc_offset
self.tz_info = timezone(timedelta(hours=utc_offset))
self.time_zone = f"UTC{utc_offset:+g}"
elif time_zone is not None:
# Timezone string provided
self.time_zone = time_zone
self.utc_offset = self._get_offset_from_name(time_zone)
self.tz_info = timezone(timedelta(hours=self.utc_offset))
else:
# Auto-detect local timezone
self.utc_offset = self._get_local_utc_offset()
self.tz_info = timezone(timedelta(hours=self.utc_offset))
self.time_zone = f"UTC{self.utc_offset:+g}"
self.time_format = '%Y-%m-%d %H:%M:%S'
@staticmethod
def _get_local_utc_offset() -> float:
"""
Automatically detect local UTC offset using only local system information.
No external communication or network access required.
Returns:
UTC offset in hours for the local system
"""
# Method 1: Calculate from local time difference
# This is most reliable across platforms
local_time = datetime.now()
utc_time = datetime.utcnow()
# Calculate the difference
delta = local_time - utc_time
offset_hours = delta.total_seconds() / 3600
# Round to nearest 30 minutes (to handle half-hour timezones)
# e.g., India (UTC+5:30), Nepal (UTC+5:45)
offset_hours = round(offset_hours * 2) / 2
# Method 2: Using time.timezone as fallback/validation
try:
if time.daylight:
# DST is in effect
offset_seconds_alt = -time.altzone
else:
# Standard time
offset_seconds_alt = -time.timezone
offset_hours_alt = offset_seconds_alt / 3600
# If results differ significantly, prefer Method 1
if abs(offset_hours - offset_hours_alt) > 0.5:
# Method 1 is usually more accurate
return offset_hours
except AttributeError:
# Some systems might not have altzone
pass
return offset_hours
@staticmethod
def _get_offset_from_name(tz_name: str) -> float:
"""
Get UTC offset from timezone name.
Args:
tz_name: Timezone name or offset string
Returns:
UTC offset in hours
"""
# Common timezone offsets (without DST consideration)
KNOWN_TIMEZONES = {
'Asia/Seoul': 9,
'Asia/Tokyo': 9,
'Asia/Shanghai': 8,
'Asia/Singapore': 8,
'Asia/Kolkata': 5.5,
'Asia/Dubai': 4,
'Europe/Moscow': 3,
'Europe/Paris': 1,
'Europe/London': 0,
'UTC': 0,
'US/Eastern': -5,
'US/Central': -6,
'US/Mountain': -7,
'US/Pacific': -8,
'America/New_York': -5,
'America/Chicago': -6,
'America/Denver': -7,
'America/Los_Angeles': -8,
}
# Check if it's a known timezone
if tz_name in KNOWN_TIMEZONES:
return KNOWN_TIMEZONES[tz_name]
# Try to parse offset strings like 'UTC+9', 'GMT-5', '+09:00'
if 'UTC' in tz_name or 'GMT' in tz_name:
try:
offset_str = tz_name.replace('UTC', '').replace('GMT', '').strip()
return float(offset_str)
except ValueError:
pass
# Handle ISO format offsets like '+09:00', '-05:30'
if ':' in tz_name and (tz_name[0] in '+-' or tz_name[-6] in '+-'):
try:
# Find the offset part
if tz_name[0] in '+-':
offset_str = tz_name
else:
offset_str = tz_name[-6:]
sign = 1 if offset_str[0] == '+' else -1
parts = offset_str[1:].split(':')
hours = int(parts[0])
minutes = int(parts[1]) if len(parts) > 1 else 0
return sign * (hours + minutes / 60)
except (ValueError, IndexError):
pass
# If all parsing fails, detect local timezone
print(f"Warning: Unknown timezone '{tz_name}', using local timezone")
return TS._get_local_utc_offset()
[docs]
def timestamp_to_datetime(self, timestamp) -> datetime:
"""
Convert timestamp to datetime object with timezone.
Args:
timestamp: Unix timestamp (int/float) or datetime object
Returns:
datetime object with timezone or None if invalid input
Example:
>>> ts = TS() # Auto-detects local timezone
>>> dt = ts.timestamp_to_datetime(1640995200)
"""
match timestamp:
case int() | float():
return datetime.fromtimestamp(timestamp, tz=self.tz_info)
case datetime():
return timestamp
case _:
return None
[docs]
def get_timezone_info(self) -> Dict[str, Any]:
"""
Get information about current timezone settings.
Returns:
Dictionary with timezone information
"""
now_local = datetime.now(self.tz_info)
now_utc = datetime.now(timezone.utc)
return {
'timezone': self.time_zone,
'utc_offset_hours': self.utc_offset,
'current_time': now_local.strftime(self.time_format),
'utc_time': now_utc.strftime(self.time_format),
'is_dst': time.daylight and time.localtime().tm_isdst > 0
}
# ============================================================================
# Progress Bar Utilities
# ============================================================================
[docs]
class ProgressBar:
"""
Progress bar utility for visual feedback during long operations.
Supports both console and Jupyter notebook environments.
"""
[docs]
@staticmethod
def show_bar(current: int, total: int, prefix: str = "Progress",
suffix: str = "", bar_length: int = 30,
filled_char: str = "█", empty_char: str = "░") -> str:
"""
Generate a text-based progress bar.
Args:
current: Current progress value
total: Total value for completion
prefix: Text to display before the bar
suffix: Text to display after the bar
bar_length: Length of the progress bar in characters
filled_char: Character for filled portion
empty_char: Character for empty portion
Returns:
Formatted progress bar string
Example:
>>> bar = ProgressBar.show_bar(30, 100, prefix="Processing")
>>> print(bar)
Processing: [█████████░░░░░░░░░░░░░░░░░░░░░] 30.0% (30/100)
"""
if total == 0:
return f"{prefix}: [{empty_char * bar_length}] 0.0% (0/0) {suffix}"
percent = (current / total) * 100
filled = int(bar_length * current / total)
bar = filled_char * filled + empty_char * (bar_length - filled)
result = f"{prefix}: [{bar}] {percent:.1f}% ({current}/{total})"
if suffix:
result += f" {suffix}"
return result
[docs]
@staticmethod
def show_spinner(current: int, prefix: str = "Processing",
suffix: str = "") -> str:
"""
Generate a simple spinner animation.
Args:
current: Current iteration count
prefix: Text before spinner
suffix: Text after spinner
Returns:
Formatted spinner string
Example:
>>> for i in range(10):
... spinner = ProgressBar.show_spinner(i, "Loading")
... print(f"\r{spinner}", end="")
"""
spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
spinner = spinners[current % len(spinners)]
result = f"{prefix} {spinner}"
if suffix:
result += f" {suffix}"
return result
[docs]
@staticmethod
def update_display(content: str, jupyter_mode: bool = False,
clear: bool = True) -> None:
"""
Update display with content, supporting both console and Jupyter.
Args:
content: Content to display
jupyter_mode: Whether running in Jupyter environment
clear: Whether to clear previous output
Example:
>>> for i in range(100):
... bar = ProgressBar.show_bar(i, 100)
... ProgressBar.update_display(bar, jupyter_mode=True)
"""
if jupyter_mode:
try:
from IPython.display import clear_output, display
if clear:
clear_output(wait=True)
print(content)
except ImportError:
# Fallback to console mode if IPython not available
if clear:
print(f"\r{content}", end="", flush=True)
else:
print(content)
else:
if clear:
# Console mode with carriage return
print(f"\r{content}", end="", flush=True)
else:
print(content)
[docs]
@staticmethod
def estimate_remaining(current: int, total: int,
elapsed_seconds: float) -> Optional[float]:
"""
Estimate remaining time based on current progress.
Args:
current: Current progress value
total: Total value for completion
elapsed_seconds: Elapsed time in seconds
Returns:
Estimated remaining time in seconds or None
Example:
>>> remaining = ProgressBar.estimate_remaining(30, 100, 15.0)
>>> print(f"Remaining: {ProgressBar.format_time(remaining)}")
"""
if current == 0 or total == 0:
return None
rate = current / elapsed_seconds
remaining_items = total - current
if rate > 0:
return remaining_items / rate
else:
return None
[docs]
class ProgressTracker:
"""
Advanced progress tracking with statistics and time estimation.
Supports status messages for richer feedback during processing.
"""
[docs]
def __init__(self, total: int, prefix: str = "Progress",
jupyter_mode: bool = False, bar_length: int = 30,
show_status: bool = True):
"""
Initialize progress tracker.
Args:
total: Total number of items to process
prefix: Prefix text for progress bar
jupyter_mode: Whether in Jupyter environment
bar_length: Length of progress bar
show_status: Whether to display status messages
"""
self.total = total
self.current = 0
self.prefix = prefix
self.jupyter_mode = jupyter_mode
self.bar_length = bar_length
self.show_status = show_status
self.start_time = time.time()
self.errors = []
self.completed_items = []
self.statuses = [] # Track status messages
self.current_status = None # Current status to display
[docs]
def update(self, increment: int = 1, item: Any = None,
error: Optional[str] = None, status: Optional[str] = None) -> None:
"""
Update progress with optional item tracking and status message.
Args:
increment: Amount to increment progress
item: Item that was processed (optional)
error: Error message if processing failed (optional)
status: Status message to display (optional)
"""
self.current += increment
if error:
self.errors.append({'item': item, 'error': error})
elif item is not None:
self.completed_items.append(item)
if status is not None:
self.statuses.append({'item': item, 'status': status})
self.current_status = status
self._display_progress()
def _display_progress(self) -> None:
"""Display current progress with statistics and status."""
elapsed = time.time() - self.start_time
# Generate progress bar
bar = ProgressBar.show_bar(
self.current, self.total,
prefix=self.prefix,
bar_length=self.bar_length
)
# Build content lines
content = [bar]
# Add current status if available
if self.show_status and self.current_status:
content.append(f"Status: {self.current_status}")
# Add time information
elapsed_str = ProgressBar.format_time(elapsed)
content.append(f"Elapsed: {elapsed_str}")
# Add remaining time estimate
remaining = ProgressBar.estimate_remaining(
self.current, self.total, elapsed
)
if remaining:
remaining_str = ProgressBar.format_time(remaining)
content.append(f"Remaining: {remaining_str}")
# Add error count if any
if self.errors:
content.append(f"Errors: {len(self.errors)}")
# Update display
display_content = "\n".join(content)
ProgressBar.update_display(
display_content,
jupyter_mode=self.jupyter_mode
)
[docs]
def get_status_summary(self) -> Dict[str, Any]:
"""
Get summary of all status messages.
Returns:
Dictionary with status statistics
"""
if not self.statuses:
return {}
# Count status types
status_counts = {}
for status_entry in self.statuses:
status_msg = status_entry['status']
status_counts[status_msg] = status_counts.get(status_msg, 0) + 1
return {
'total_statuses': len(self.statuses),
'unique_statuses': len(status_counts),
'status_counts': status_counts,
'recent_statuses': self.statuses[-5:] # Last 5 statuses
}
[docs]
def finish(self, show_summary: bool = True) -> Dict[str, Any]:
"""
Finish tracking and return summary.
Args:
show_summary: Whether to display summary
Returns:
Dictionary with tracking statistics including status summary
"""
elapsed = time.time() - self.start_time
summary = {
'total': self.total,
'completed': self.current,
'errors': len(self.errors),
'elapsed_time': elapsed,
'elapsed_formatted': ProgressBar.format_time(elapsed),
'items_per_second': self.current / elapsed if elapsed > 0 else 0,
'error_details': self.errors,
'completed_items': self.completed_items,
'status_summary': self.get_status_summary() # Add status summary
}
if show_summary:
print() # New line after progress bar
print("=" * 50)
print("Completion Summary")
print("=" * 50)
print(f"Processed: {summary['completed']}/{summary['total']}")
print(f"Time taken: {summary['elapsed_formatted']}")
print(f"Processing rate: {summary['items_per_second']:.2f} items/sec")
# Show status summary if available
if summary['status_summary'] and summary['status_summary'].get('status_counts'):
print("\nResults by status:")
for status, count in summary['status_summary']['status_counts'].items():
print(f" • {status}: {count}")
if summary['errors']:
print(f"\nErrors: {summary['errors']}")
# Show first few errors
for i, error_entry in enumerate(summary['error_details'][:3]):
print(f" - {error_entry['item']}: {error_entry['error']}")
if len(summary['error_details']) > 3:
print(f" ... and {len(summary['error_details']) - 3} more")
return summary
[docs]
def diff_codes(left: str, right: str, mode: int = 0):
"""
Compare two code strings with different diff formats.
Args:
left: Left code string to compare
right: Right code string to compare
mode: Comparison mode (0=simple, 1=unified, 2=ndiff)
Example:
>>> diff_codes("line1\\nline2", "line1\\nmodified", mode=1)
"""
left_lines = left.splitlines()
right_lines = right.splitlines()
match mode:
case 0:
print("\n=== simple mode ===\n")
# Simple line-by-line comparison
for i, (l, r) in enumerate(zip(left_lines, right_lines), start=1):
if l != r:
print(f"Difference found at line {i}:")
print(f"Left: {l}")
print(f"Right: {r}")
print()
# Handle different line counts
if len(left_lines) > len(right_lines):
print("Additional lines in left code:")
for i, l in enumerate(left_lines[len(right_lines):], start=len(right_lines) + 1):
print(f"Line {i}: {l}")
elif len(right_lines) > len(left_lines):
print("Additional lines in right code:")
for i, r in enumerate(right_lines[len(left_lines):], start=len(left_lines) + 1):
print(f"Line {i}: {r}")
case 1:
print("\n=== unified mode ===\n")
# Unified diff format
diff = difflib.unified_diff(
left_lines, right_lines,
fromfile='left', tofile='right',
lineterm=''
)
print("\n".join(diff))
case 2:
print("\n=== ndiff mode ===")
# Detailed ndiff format
diff = difflib.ndiff(left_lines, right_lines)
print("\n".join(diff))
case _:
print("Unsupported mode. Please choose 0 (simple), 1 (unified), or 2 (ndiff).")
[docs]
def import_script(script_name: str, script_path: str):
"""
Dynamically import a Python module from file path.
Args:
script_name: Name for the imported module
script_path: Path to the Python file to import
Returns:
Imported module object
Example:
>>> module = import_script("my_module", "/path/to/script.py")
>>> module.some_function()
"""
module_spec = ut.spec_from_file_location(script_name, script_path)
module = ut.module_from_spec(module_spec)
module_dir = os.path.dirname(script_path)
prev_cwd = os.getcwd()
os.chdir(module_dir)
try:
module_spec.loader.exec_module(module)
finally:
os.chdir(prev_cwd)
return module
[docs]
def flatten(lst, max_depth=1, current_depth=0):
"""
Flatten nested lists up to a specified depth.
Args:
lst: The list to flatten
max_depth: Maximum depth to flatten (default: 1)
current_depth: Current recursion depth (internal use)
Returns:
Flattened list
Example:
>>> flatten([1, [2, [3, 4], 5], [6, 7], 8])
[1, 2, 3, 4, 5, 6, 7, 8]
"""
result = []
for item in lst:
if isinstance(item, list) and current_depth < max_depth:
result.extend(flatten(item, max_depth, current_depth + 1))
else:
result.append(item)
return result
[docs]
def flatten_gen(lst, max_depth=1, current_depth=0):
"""
Flatten nested lists using generator (memory efficient).
Args:
lst: The list to flatten
max_depth: Maximum depth to flatten (default: 1)
current_depth: Current recursion depth (internal use)
Yields:
Flattened items one by one
Example:
>>> list(flatten_gen([1, [2, [3, [4]], 5]]))
[1, 2, 3, 4, 5]
"""
for item in lst:
if isinstance(item, list) and current_depth < max_depth:
yield from flatten_gen(item, max_depth, current_depth + 1)
else:
yield item
[docs]
def flatten_any(nested, max_depth=1, current_depth=0):
"""
Flatten nested collections (list, tuple, set) up to specified depth.
Args:
nested: The nested collection to flatten
max_depth: Maximum depth to flatten (default: 1)
current_depth: Current recursion depth (internal use)
Yields:
Flattened items one by one
Example:
>>> list(flatten_any([1, (2, [3, {4, 5}])]))
[1, 2, 3, 4, 5] # Order may vary for set items
"""
for item in nested:
if isinstance(item, (list, tuple, set)) and current_depth < max_depth:
yield from flatten_any(item, max_depth, current_depth + 1)
else:
yield item
[docs]
def flatten_three_levels_with_suffix(nested_dict: dict) -> dict:
"""
Flatten 3-level nested dictionary by merging level2 into level1
with suffix notation for original parent keys.
Args:
nested_dict: 3-level nested dictionary
Returns:
Flattened dictionary with suffix notation
Example:
>>> data = {'A': {'x': 1, 'y': {'p': 10, 'q': 20}, 'z': 3}}
>>> flatten_three_levels_with_suffix(data)
{'A': {'x': 1, 'p (y)': 10, 'q (y)': 20, 'z': 3}}
"""
result = {}
for (top_key, level1) in nested_dict.items():
if not isinstance(level1, dict):
result[top_key] = level1
continue
merged = {}
for (k1, v1) in level1.items():
if isinstance(v1, dict):
# Level2 dict: extract items with suffix
for (k2, v2) in v1.items():
new_key = f"{k2} ({k1})"
merged[new_key] = v2
else:
merged[k1] = v1
result[top_key] = merged
return result
# ============================================================================
# Network Utilities (WOL)
# ============================================================================
[docs]
class WOL:
"""Wake-on-LAN utility for network device control."""
[docs]
def __init__(self, verbose: bool = True):
"""
Initialize Wake-on-LAN handler.
Args:
verbose: Enable detailed output messages (default: True)
"""
self.verbose = verbose
self._socket = None # Lazy load socket
def _ensure_socket(self):
"""Lazy import socket module when needed."""
if self._socket is None:
try:
import socket
self._socket = socket
except ImportError:
raise ImportError(
"WOL functionality requires socket library. "
"This should be available in standard Python installation."
)
[docs]
def validate_mac(self, mac: str) -> bool:
"""
Validate MAC address format.
Args:
mac: MAC address string
Returns:
True if valid MAC address format
Example:
>>> wol = WOL()
>>> wol.validate_mac("AA:BB:CC:DD:EE:FF")
True
>>> wol.validate_mac("AA-BB-CC-DD-EE-FF")
True
"""
# Support various MAC formats: XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, XXXXXXXXXXXX
mac_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})$')
return bool(mac_pattern.match(mac.replace(' ', '')))
[docs]
def create_magic_packet(self, mac: str) -> bytes:
"""
Create WOL magic packet.
Magic packet structure:
- 6 bytes of 0xFF (synchronization stream)
- Target MAC address repeated 16 times
Args:
mac: Target device MAC address
Returns:
Magic packet as bytes
Example:
>>> wol = WOL()
>>> packet = wol.create_magic_packet("AA:BB:CC:DD:EE:FF")
>>> len(packet)
102
"""
mac_formatted = self.format_mac(mac)
mac_bytes = bytes.fromhex(mac_formatted.replace(':', ''))
# Magic packet: 0xFF * 6 + MAC * 16
return b'\xff' * 6 + mac_bytes * 16
[docs]
def send_packet(self,
mac: str,
broadcast_ip: str = '255.255.255.255',
port: int = 9,
attempts: int = 3,
delay: float = 0.5) -> bool:
"""
Send WOL magic packet to network.
Args:
mac: Target device MAC address
broadcast_ip: Broadcast IP address (default: 255.255.255.255)
port: WOL port (default: 9, alternative: 7)
attempts: Number of send attempts (default: 3)
delay: Delay between attempts in seconds (default: 0.5)
Returns:
True if packet sent successfully
Example:
>>> wol = WOL()
>>> success = wol.send_packet("AA:BB:CC:DD:EE:FF")
"""
# Ensure socket is imported
self._ensure_socket()
if not self.validate_mac(mac):
raise ValueError(f"Invalid MAC address format: {mac}")
packet = self.create_magic_packet(mac)
# Create UDP socket with broadcast enabled
with self._socket.socket(self._socket.AF_INET, self._socket.SOCK_DGRAM) as sock:
sock.setsockopt(self._socket.SOL_SOCKET, self._socket.SO_BROADCAST, 1)
for attempt in range(attempts):
try:
sock.sendto(packet, (broadcast_ip, port))
if self.verbose:
print(f" [{attempt + 1}/{attempts}] Magic packet sent to {broadcast_ip}:{port}")
if attempt < attempts - 1:
time.sleep(delay)
except Exception as e:
if self.verbose:
print(f" [{attempt + 1}/{attempts}] Send failed: {e}")
return False
return True
[docs]
def wake(self,
mac: str,
device_name: Optional[str] = None,
subnet_broadcast: Optional[str] = None) -> bool:
"""
Wake network device using Wake-on-LAN.
Args:
mac: Target device MAC address
device_name: Device name for display (optional)
subnet_broadcast: Subnet broadcast address (e.g., 192.168.1.255)
Returns:
True if wake signals sent successfully
Example:
>>> wol = WOL()
>>> wol.wake("AA:BB:CC:DD:EE:FF", device_name="Development Server")
>>> # With subnet broadcast
>>> wol.wake("AA:BB:CC:DD:EE:FF",
... device_name="Office PC",
... subnet_broadcast="192.168.1.255")
"""
success = True
if self.verbose:
print("=" * 50)
print("Wake-on-LAN")
print("=" * 50)
if device_name:
print(f"Target device: {device_name}")
try:
mac_formatted = self.format_mac(mac)
print(f"MAC address: {mac_formatted}")
except ValueError as e:
print(f"Error: {e}")
return False
print("-" * 50)
# Send via global broadcast
if self.verbose:
print("Sending via global broadcast (255.255.255.255)...")
if not self.send_packet(mac):
success = False
if self.verbose:
print("Global broadcast failed")
elif self.verbose:
print("Global broadcast sent successfully")
# Send via subnet broadcast if provided
if subnet_broadcast:
if self.verbose:
print(f"\nSending via subnet broadcast ({subnet_broadcast})...")
if not self.send_packet(mac, broadcast_ip=subnet_broadcast):
success = False
if self.verbose:
print("Subnet broadcast failed")
elif self.verbose:
print("Subnet broadcast sent successfully")
if self.verbose:
print("-" * 50)
if success:
print("Wake signal sent. Device should power on within 10-30 seconds.")
else:
print("Failed to send wake signal.")
print("=" * 50)
return success
[docs]
def wake_device(mac: str,
device_name: Optional[str] = None,
subnet_broadcast: Optional[str] = None,
verbose: bool = True) -> bool:
"""
Wake network device using Wake-on-LAN (convenience function).
Args:
mac: Target device MAC address
device_name: Device name for display (optional)
subnet_broadcast: Subnet broadcast address (optional)
verbose: Enable detailed output (default: True)
Returns:
True if wake signals sent successfully
Example:
>>> # Simple usage
>>> wake_device("AA:BB:CC:DD:EE:FF")
>>> # With device name and subnet
>>> wake_device("AA:BB:CC:DD:EE:FF",
... device_name="Development Server",
... subnet_broadcast="192.168.1.255")
>>> # Silent mode
>>> success = wake_device("AA:BB:CC:DD:EE:FF", verbose=False)
"""
wol = WOL(verbose=verbose)
return wol.wake(mac, device_name, subnet_broadcast)
[docs]
def wake_multiple_devices(devices: Dict[str, str],
subnet_broadcast: Optional[str] = None,
verbose: bool = True,
delay: float = 1.0) -> Dict[str, bool]:
"""
Wake multiple network devices sequentially.
Args:
devices: Dictionary of {device_name: mac_address}
subnet_broadcast: Subnet broadcast address (optional)
verbose: Enable detailed output (default: True)
delay: Delay between devices in seconds (default: 1.0)
Returns:
Dictionary of {device_name: success_status}
Example:
>>> devices = {
... "Development Server": "AA:BB:CC:DD:EE:FF",
... "Testing Machine": "11:22:33:44:55:66",
... "Database Server": "99:88:77:66:55:44"
... }
>>> results = wake_multiple_devices(devices, subnet_broadcast="192.168.1.255")
>>> for device, success in results.items():
... status = "OK" if success else "FAILED"
... print(f"{device}: {status}")
"""
wol = WOL(verbose=verbose)
results = {}
for i, (name, mac) in enumerate(devices.items()):
if i > 0:
time.sleep(delay)
if verbose:
print("\n")
results[name] = wol.wake(mac, device_name=name, subnet_broadcast=subnet_broadcast)
return results