eeroctl 1.7.1__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.
- eeroctl/__init__.py +19 -0
- eeroctl/commands/__init__.py +32 -0
- eeroctl/commands/activity.py +237 -0
- eeroctl/commands/auth.py +471 -0
- eeroctl/commands/completion.py +142 -0
- eeroctl/commands/device.py +492 -0
- eeroctl/commands/eero/__init__.py +12 -0
- eeroctl/commands/eero/base.py +224 -0
- eeroctl/commands/eero/led.py +154 -0
- eeroctl/commands/eero/nightlight.py +235 -0
- eeroctl/commands/eero/updates.py +82 -0
- eeroctl/commands/network/__init__.py +18 -0
- eeroctl/commands/network/advanced.py +191 -0
- eeroctl/commands/network/backup.py +162 -0
- eeroctl/commands/network/base.py +331 -0
- eeroctl/commands/network/dhcp.py +118 -0
- eeroctl/commands/network/dns.py +197 -0
- eeroctl/commands/network/forwards.py +115 -0
- eeroctl/commands/network/guest.py +162 -0
- eeroctl/commands/network/security.py +162 -0
- eeroctl/commands/network/speedtest.py +99 -0
- eeroctl/commands/network/sqm.py +194 -0
- eeroctl/commands/profile.py +671 -0
- eeroctl/commands/troubleshoot.py +317 -0
- eeroctl/context.py +254 -0
- eeroctl/errors.py +156 -0
- eeroctl/exit_codes.py +68 -0
- eeroctl/formatting/__init__.py +90 -0
- eeroctl/formatting/base.py +181 -0
- eeroctl/formatting/device.py +430 -0
- eeroctl/formatting/eero.py +591 -0
- eeroctl/formatting/misc.py +87 -0
- eeroctl/formatting/network.py +659 -0
- eeroctl/formatting/profile.py +443 -0
- eeroctl/main.py +161 -0
- eeroctl/options.py +429 -0
- eeroctl/output.py +739 -0
- eeroctl/safety.py +259 -0
- eeroctl/utils.py +181 -0
- eeroctl-1.7.1.dist-info/METADATA +115 -0
- eeroctl-1.7.1.dist-info/RECORD +45 -0
- eeroctl-1.7.1.dist-info/WHEEL +5 -0
- eeroctl-1.7.1.dist-info/entry_points.txt +3 -0
- eeroctl-1.7.1.dist-info/licenses/LICENSE +21 -0
- eeroctl-1.7.1.dist-info/top_level.txt +1 -0
eeroctl/errors.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Centralized error handling for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for translating exceptions to user-friendly
|
|
4
|
+
error messages and appropriate exit codes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, TypeVar
|
|
8
|
+
|
|
9
|
+
from eero.exceptions import (
|
|
10
|
+
EeroAPIException,
|
|
11
|
+
EeroAuthenticationException,
|
|
12
|
+
EeroException,
|
|
13
|
+
EeroFeatureUnavailableException,
|
|
14
|
+
EeroNotFoundException,
|
|
15
|
+
EeroPremiumRequiredException,
|
|
16
|
+
EeroRateLimitException,
|
|
17
|
+
EeroTimeoutException,
|
|
18
|
+
EeroValidationException,
|
|
19
|
+
)
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
from .exit_codes import ExitCode
|
|
23
|
+
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_cli_error(
|
|
28
|
+
e: Exception,
|
|
29
|
+
console: Console,
|
|
30
|
+
renderer: Optional[object] = None,
|
|
31
|
+
context: str = "",
|
|
32
|
+
) -> int:
|
|
33
|
+
"""Handle an exception and return the appropriate exit code.
|
|
34
|
+
|
|
35
|
+
This function translates exceptions into user-friendly error messages
|
|
36
|
+
and determines the correct exit code.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
e: The exception to handle
|
|
40
|
+
console: Rich console for output
|
|
41
|
+
renderer: Optional OutputRenderer for JSON output
|
|
42
|
+
context: Optional context string for better error messages
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The exit code to use
|
|
46
|
+
"""
|
|
47
|
+
prefix = f"{context}: " if context else ""
|
|
48
|
+
|
|
49
|
+
if isinstance(e, EeroAuthenticationException):
|
|
50
|
+
console.print(f"[red]{prefix}Authentication required. Run 'eero auth login' first.[/red]")
|
|
51
|
+
return ExitCode.AUTH_REQUIRED
|
|
52
|
+
|
|
53
|
+
elif isinstance(e, EeroNotFoundException):
|
|
54
|
+
console.print(f"[red]{prefix}{e.resource_type} '{e.resource_id}' not found[/red]")
|
|
55
|
+
return ExitCode.NOT_FOUND
|
|
56
|
+
|
|
57
|
+
elif isinstance(e, EeroPremiumRequiredException):
|
|
58
|
+
console.print(f"[yellow]{prefix}{e.feature} requires Eero Plus subscription[/yellow]")
|
|
59
|
+
return ExitCode.PREMIUM_REQUIRED
|
|
60
|
+
|
|
61
|
+
elif isinstance(e, EeroFeatureUnavailableException):
|
|
62
|
+
console.print(f"[yellow]{prefix}{e.feature} is {e.reason}[/yellow]")
|
|
63
|
+
return ExitCode.FEATURE_UNAVAILABLE
|
|
64
|
+
|
|
65
|
+
elif isinstance(e, EeroRateLimitException):
|
|
66
|
+
console.print(f"[yellow]{prefix}Rate limited. Please wait and try again.[/yellow]")
|
|
67
|
+
return ExitCode.TIMEOUT
|
|
68
|
+
|
|
69
|
+
elif isinstance(e, EeroTimeoutException):
|
|
70
|
+
console.print(f"[red]{prefix}Request timed out. Check your connection and try again.[/red]")
|
|
71
|
+
return ExitCode.TIMEOUT
|
|
72
|
+
|
|
73
|
+
elif isinstance(e, EeroValidationException):
|
|
74
|
+
console.print(f"[red]{prefix}Invalid input for '{e.field}': {e.message}[/red]")
|
|
75
|
+
return ExitCode.USAGE_ERROR
|
|
76
|
+
|
|
77
|
+
elif isinstance(e, EeroAPIException):
|
|
78
|
+
# Map HTTP status codes to exit codes
|
|
79
|
+
if e.status_code == 401:
|
|
80
|
+
console.print(
|
|
81
|
+
f"[red]{prefix}Session expired. Run 'eero auth login' to re-authenticate.[/red]"
|
|
82
|
+
)
|
|
83
|
+
return ExitCode.AUTH_REQUIRED
|
|
84
|
+
elif e.status_code == 403:
|
|
85
|
+
console.print(f"[red]{prefix}Permission denied: {e.message}[/red]")
|
|
86
|
+
return ExitCode.FORBIDDEN
|
|
87
|
+
elif e.status_code == 404:
|
|
88
|
+
console.print(f"[red]{prefix}Resource not found: {e.message}[/red]")
|
|
89
|
+
return ExitCode.NOT_FOUND
|
|
90
|
+
elif e.status_code == 409:
|
|
91
|
+
console.print(f"[yellow]{prefix}Conflict: {e.message}[/yellow]")
|
|
92
|
+
return ExitCode.CONFLICT
|
|
93
|
+
elif e.status_code == 429:
|
|
94
|
+
console.print(f"[yellow]{prefix}Rate limited. Please wait and try again.[/yellow]")
|
|
95
|
+
return ExitCode.TIMEOUT
|
|
96
|
+
else:
|
|
97
|
+
console.print(f"[red]{prefix}API error ({e.status_code}): {e.message}[/red]")
|
|
98
|
+
return ExitCode.GENERIC_ERROR
|
|
99
|
+
|
|
100
|
+
elif isinstance(e, EeroException):
|
|
101
|
+
# Generic Eero exception
|
|
102
|
+
console.print(f"[red]{prefix}{e.message}[/red]")
|
|
103
|
+
return ExitCode.GENERIC_ERROR
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
# Unknown exception
|
|
107
|
+
console.print(f"[red]{prefix}Unexpected error: {e}[/red]")
|
|
108
|
+
return ExitCode.GENERIC_ERROR
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_premium_error(e: Exception) -> bool:
|
|
112
|
+
"""Check if an exception indicates a premium feature requirement.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
e: Exception to check
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if this is a premium-required error
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(e, EeroPremiumRequiredException):
|
|
121
|
+
return True
|
|
122
|
+
# Fallback string matching for generic exceptions
|
|
123
|
+
error_str = str(e).lower()
|
|
124
|
+
return "premium" in error_str or "plus" in error_str or "subscription" in error_str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_feature_unavailable_error(e: Exception, feature_keyword: str) -> bool:
|
|
128
|
+
"""Check if an exception indicates a feature is unavailable.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
e: Exception to check
|
|
132
|
+
feature_keyword: Keyword to look for in error message
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if this is a feature-unavailable error
|
|
136
|
+
"""
|
|
137
|
+
if isinstance(e, EeroFeatureUnavailableException):
|
|
138
|
+
return True
|
|
139
|
+
# Fallback string matching for generic exceptions
|
|
140
|
+
return feature_keyword.lower() in str(e).lower()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_not_found_error(e: Exception) -> bool:
|
|
144
|
+
"""Check if an exception indicates a resource was not found.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
e: Exception to check
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if this is a not-found error
|
|
151
|
+
"""
|
|
152
|
+
if isinstance(e, EeroNotFoundException):
|
|
153
|
+
return True
|
|
154
|
+
if isinstance(e, EeroAPIException) and e.status_code == 404:
|
|
155
|
+
return True
|
|
156
|
+
return "not found" in str(e).lower()
|
eeroctl/exit_codes.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Exit codes for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
Standard exit codes for consistent behavior across all commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExitCode(IntEnum):
|
|
10
|
+
"""Standard exit codes for the CLI.
|
|
11
|
+
|
|
12
|
+
Exit codes must be consistent across all commands for scripting.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
SUCCESS = 0
|
|
16
|
+
"""Command completed successfully."""
|
|
17
|
+
|
|
18
|
+
GENERIC_ERROR = 1
|
|
19
|
+
"""Generic/unexpected error."""
|
|
20
|
+
|
|
21
|
+
USAGE_ERROR = 2
|
|
22
|
+
"""Invalid arguments or command usage."""
|
|
23
|
+
|
|
24
|
+
AUTH_REQUIRED = 3
|
|
25
|
+
"""Authentication required or session expired."""
|
|
26
|
+
|
|
27
|
+
FORBIDDEN = 4
|
|
28
|
+
"""Permission denied/forbidden operation."""
|
|
29
|
+
|
|
30
|
+
NOT_FOUND = 5
|
|
31
|
+
"""Resource not found (ID, name, MAC address)."""
|
|
32
|
+
|
|
33
|
+
CONFLICT = 6
|
|
34
|
+
"""Conflict or invalid state (e.g., already enabled)."""
|
|
35
|
+
|
|
36
|
+
TIMEOUT = 7
|
|
37
|
+
"""Request or operation timed out."""
|
|
38
|
+
|
|
39
|
+
SAFETY_RAIL = 8
|
|
40
|
+
"""Safety rail triggered - needs --force or confirmation."""
|
|
41
|
+
|
|
42
|
+
# Reserved: 9
|
|
43
|
+
|
|
44
|
+
PARTIAL_SUCCESS = 10
|
|
45
|
+
"""Multi-target operation had partial success."""
|
|
46
|
+
|
|
47
|
+
PREMIUM_REQUIRED = 11
|
|
48
|
+
"""Feature requires Eero Plus subscription."""
|
|
49
|
+
|
|
50
|
+
FEATURE_UNAVAILABLE = 12
|
|
51
|
+
"""Feature is not available on this device or network."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Exit code descriptions for help text
|
|
55
|
+
EXIT_CODE_DESCRIPTIONS = {
|
|
56
|
+
ExitCode.SUCCESS: "Operation completed successfully",
|
|
57
|
+
ExitCode.GENERIC_ERROR: "An unexpected error occurred",
|
|
58
|
+
ExitCode.USAGE_ERROR: "Invalid command usage or arguments",
|
|
59
|
+
ExitCode.AUTH_REQUIRED: "Authentication required or session expired",
|
|
60
|
+
ExitCode.FORBIDDEN: "Permission denied for this operation",
|
|
61
|
+
ExitCode.NOT_FOUND: "Requested resource not found",
|
|
62
|
+
ExitCode.CONFLICT: "Operation conflicts with current state",
|
|
63
|
+
ExitCode.TIMEOUT: "Operation timed out",
|
|
64
|
+
ExitCode.SAFETY_RAIL: "Safety confirmation required (use --force)",
|
|
65
|
+
ExitCode.PARTIAL_SUCCESS: "Operation partially completed",
|
|
66
|
+
ExitCode.PREMIUM_REQUIRED: "Feature requires Eero Plus subscription",
|
|
67
|
+
ExitCode.FEATURE_UNAVAILABLE: "Feature not available on this device",
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Formatting utilities for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
This module provides reusable formatting functions for displaying
|
|
4
|
+
Eero network data using Rich panels and tables.
|
|
5
|
+
|
|
6
|
+
This __init__.py re-exports all public functions from submodules
|
|
7
|
+
for backward compatibility with existing imports.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Base utilities
|
|
11
|
+
from .base import (
|
|
12
|
+
DetailLevel,
|
|
13
|
+
build_panel,
|
|
14
|
+
console,
|
|
15
|
+
field,
|
|
16
|
+
field_bool,
|
|
17
|
+
field_status,
|
|
18
|
+
format_bool,
|
|
19
|
+
format_datetime,
|
|
20
|
+
format_device_status,
|
|
21
|
+
format_eero_status,
|
|
22
|
+
format_enabled,
|
|
23
|
+
format_network_status,
|
|
24
|
+
get_network_status_value,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Device formatting
|
|
28
|
+
from .device import (
|
|
29
|
+
create_devices_table,
|
|
30
|
+
print_device_details,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Eero device formatting
|
|
34
|
+
from .eero import (
|
|
35
|
+
create_eeros_table,
|
|
36
|
+
print_eero_details,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Miscellaneous formatting
|
|
40
|
+
from .misc import (
|
|
41
|
+
create_blacklist_table,
|
|
42
|
+
print_speedtest_results,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Network formatting
|
|
46
|
+
from .network import (
|
|
47
|
+
create_network_table,
|
|
48
|
+
print_network_details,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Profile formatting
|
|
52
|
+
from .profile import (
|
|
53
|
+
create_profile_devices_table,
|
|
54
|
+
create_profiles_table,
|
|
55
|
+
print_profile_details,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Re-export all public names
|
|
59
|
+
__all__ = [
|
|
60
|
+
# Base
|
|
61
|
+
"console",
|
|
62
|
+
"DetailLevel",
|
|
63
|
+
"get_network_status_value",
|
|
64
|
+
"format_network_status",
|
|
65
|
+
"format_device_status",
|
|
66
|
+
"format_eero_status",
|
|
67
|
+
"format_bool",
|
|
68
|
+
"format_enabled",
|
|
69
|
+
"build_panel",
|
|
70
|
+
"field",
|
|
71
|
+
"field_bool",
|
|
72
|
+
"field_status",
|
|
73
|
+
"format_datetime",
|
|
74
|
+
# Network
|
|
75
|
+
"create_network_table",
|
|
76
|
+
"print_network_details",
|
|
77
|
+
# Eero
|
|
78
|
+
"create_eeros_table",
|
|
79
|
+
"print_eero_details",
|
|
80
|
+
# Device
|
|
81
|
+
"create_devices_table",
|
|
82
|
+
"print_device_details",
|
|
83
|
+
# Profile
|
|
84
|
+
"create_profiles_table",
|
|
85
|
+
"create_profile_devices_table",
|
|
86
|
+
"print_profile_details",
|
|
87
|
+
# Misc
|
|
88
|
+
"print_speedtest_results",
|
|
89
|
+
"create_blacklist_table",
|
|
90
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Base formatting utilities for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
This module provides shared formatting functions, constants, and helpers
|
|
4
|
+
used across all formatting modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Literal, Optional
|
|
8
|
+
|
|
9
|
+
from eero.const import EeroDeviceStatus
|
|
10
|
+
from eero.models.network import Network
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
# Create console for rich output
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
# Type alias for detail level
|
|
18
|
+
DetailLevel = Literal["brief", "full"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ==================== Status Formatting Helpers ====================
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_network_status_value(network: Network) -> str:
|
|
25
|
+
"""Extract the status value from a network, handling both enum and string types.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
network: Network model instance
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Status string value (e.g., "online", "offline")
|
|
32
|
+
"""
|
|
33
|
+
if not network.status:
|
|
34
|
+
return "unknown"
|
|
35
|
+
# Handle both enum (has .value) and string types
|
|
36
|
+
if hasattr(network.status, "value"):
|
|
37
|
+
return str(network.status.value)
|
|
38
|
+
return str(network.status)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_network_status(status_value: str) -> tuple[str, str]:
|
|
42
|
+
"""Format network status into display text and style.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
status_value: Clean status string (e.g., "online", "offline", "updating")
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (display_text, style)
|
|
49
|
+
"""
|
|
50
|
+
status_lower = status_value.lower()
|
|
51
|
+
if status_lower in ("online", "connected"):
|
|
52
|
+
return "online", "green"
|
|
53
|
+
elif status_lower == "offline":
|
|
54
|
+
return "offline", "red"
|
|
55
|
+
elif status_lower == "updating":
|
|
56
|
+
return "updating", "yellow"
|
|
57
|
+
return status_value or "unknown", "dim"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_device_status(status: EeroDeviceStatus) -> tuple[str, str]:
|
|
61
|
+
"""Format device status into display text and style.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
status: Device status enum
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (display_text, style)
|
|
68
|
+
"""
|
|
69
|
+
if status == EeroDeviceStatus.CONNECTED:
|
|
70
|
+
return "connected", "green"
|
|
71
|
+
elif status == EeroDeviceStatus.BLOCKED:
|
|
72
|
+
return "blocked", "red"
|
|
73
|
+
elif status == EeroDeviceStatus.DISCONNECTED:
|
|
74
|
+
return "disconnected", "yellow"
|
|
75
|
+
return "unknown", "dim"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_eero_status(status: str) -> tuple[str, str]:
|
|
79
|
+
"""Format eero status into display text and style.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
status: Eero status string
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple of (display_text, style)
|
|
86
|
+
"""
|
|
87
|
+
if status == "green":
|
|
88
|
+
return status, "green"
|
|
89
|
+
return status, "red"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_bool(value: bool, true_text: str = "Yes", false_text: str = "No") -> str:
|
|
93
|
+
"""Format a boolean value with color coding.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Boolean value
|
|
97
|
+
true_text: Text to display when True
|
|
98
|
+
false_text: Text to display when False
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Formatted string
|
|
102
|
+
"""
|
|
103
|
+
if value:
|
|
104
|
+
return f"[green]{true_text}[/green]"
|
|
105
|
+
return f"[dim]{false_text}[/dim]"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def format_enabled(value: bool) -> str:
|
|
109
|
+
"""Format an enabled/disabled value."""
|
|
110
|
+
return format_bool(value, "Enabled", "Disabled")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ==================== Panel Building Helpers ====================
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_panel(lines: List[str], title: str, border_style: str = "blue") -> Panel:
|
|
117
|
+
"""Build a panel from a list of formatted lines.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
lines: List of formatted text lines
|
|
121
|
+
title: Panel title
|
|
122
|
+
border_style: Rich border style
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Rich Panel object
|
|
126
|
+
"""
|
|
127
|
+
# Filter out None and empty lines
|
|
128
|
+
content = "\n".join(line for line in lines if line)
|
|
129
|
+
return Panel(content, title=title, border_style=border_style)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def field(label: str, value: Any, default: str = "Unknown") -> str:
|
|
133
|
+
"""Format a single field line.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
label: Field label
|
|
137
|
+
value: Field value
|
|
138
|
+
default: Default value if value is None/empty
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Formatted line string
|
|
142
|
+
"""
|
|
143
|
+
display_value = value if value is not None else default
|
|
144
|
+
if display_value == "":
|
|
145
|
+
display_value = default
|
|
146
|
+
return f"[bold]{label}:[/bold] {display_value}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def field_bool(label: str, value: Optional[bool]) -> str:
|
|
150
|
+
"""Format a boolean field line."""
|
|
151
|
+
return f"[bold]{label}:[/bold] {format_enabled(bool(value)) if value is not None else '[dim]Unknown[/dim]'}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def field_status(label: str, text: str, style: str) -> str:
|
|
155
|
+
"""Format a status field line with color."""
|
|
156
|
+
return f"[bold]{label}:[/bold] [{style}]{text}[/{style}]"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ==================== Date/Time Formatting ====================
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def format_datetime(dt: Any, include_time: bool = True) -> str:
|
|
163
|
+
"""Format a datetime value for display.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
dt: Datetime object or string
|
|
167
|
+
include_time: Whether to include time component
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Formatted datetime string
|
|
171
|
+
"""
|
|
172
|
+
if dt is None:
|
|
173
|
+
return "Unknown"
|
|
174
|
+
if hasattr(dt, "strftime"):
|
|
175
|
+
fmt = "%Y-%m-%d %H:%M:%S" if include_time else "%Y-%m-%d"
|
|
176
|
+
return dt.strftime(fmt)
|
|
177
|
+
# Handle string datetime
|
|
178
|
+
dt_str = str(dt)
|
|
179
|
+
if include_time:
|
|
180
|
+
return dt_str[:19].replace("T", " ")
|
|
181
|
+
return dt_str[:10]
|