xenfra 0.4.3__py3-none-any.whl → 0.4.4__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.
xenfra/utils/errors.py CHANGED
@@ -1,116 +1,116 @@
1
- """Human-friendly error messages with actionable solutions."""
2
-
3
- from rich.console import Console
4
-
5
- console = Console()
6
-
7
-
8
- ERROR_SOLUTIONS = {
9
- "port_in_use": {
10
- "message": "Port {port} is already in use on the droplet",
11
- "solution": "Change the port in xenfra.yaml or stop the conflicting service",
12
- "command": "ssh root@{{ip}} 'lsof -i :{port}' # Find process using port",
13
- },
14
- "missing_dependency": {
15
- "message": "Missing dependency: {package}",
16
- "solution": "Add {package} to dependencies in {file}",
17
- "command": "uv add {package} # OR: echo '{package}' >> requirements.txt",
18
- },
19
- "ssh_failure": {
20
- "message": "Cannot connect to droplet via SSH",
21
- "solution": "Check firewall rules, wait for droplet boot, or verify SSH keys",
22
- "command": "ssh -v root@{ip} # Verbose SSH for debugging",
23
- },
24
- "docker_build_failed": {
25
- "message": "Docker build failed",
26
- "solution": "Check Dockerfile syntax and base image availability",
27
- "command": "docker build . --no-cache # Test locally",
28
- },
29
- "health_check_failed": {
30
- "message": "Application failed health check",
31
- "solution": "Ensure your app responds on port {port} at /health or /",
32
- "command": "curl http://{{ip}}:{port}/health",
33
- },
34
- "out_of_memory": {
35
- "message": "Container out of memory",
36
- "solution": "Upgrade to a larger instance size in xenfra.yaml",
37
- "command": "docker stats # Check memory usage",
38
- },
39
- }
40
-
41
-
42
- def show_error_with_solution(error_type: str, **kwargs) -> None:
43
- """
44
- Display error with actionable solution.
45
-
46
- Args:
47
- error_type: Key from ERROR_SOLUTIONS
48
- **kwargs: Template variables (port, ip, package, file, etc.)
49
- """
50
- error = ERROR_SOLUTIONS.get(error_type)
51
-
52
- if not error:
53
- # Fallback for unknown errors
54
- console.print(f"[red]❌ Error: {error_type}[/red]")
55
- return
56
-
57
- # Format message with provided kwargs
58
- try:
59
- message = error["message"].format(**kwargs)
60
- solution = error["solution"].format(**kwargs)
61
- command = error.get("command", "").format(**kwargs)
62
- except KeyError as e:
63
- console.print(f"[red]❌ Error formatting message: missing {e}[/red]")
64
- return
65
-
66
- console.print()
67
- console.print(f"[red]❌ {message}[/red]")
68
- console.print(f"[yellow]💡 Solution: {solution}[/yellow]")
69
-
70
- if command:
71
- console.print(f"[dim]Try: {command}[/dim]")
72
- console.print()
73
-
74
-
75
- def detect_error_type(error_message: str) -> tuple[str, dict]:
76
- """
77
- Attempt to detect error type from message.
78
-
79
- Returns:
80
- (error_type, kwargs) for show_error_with_solution()
81
- """
82
- error_lower = error_message.lower()
83
-
84
- # Port detection
85
- if "port" in error_lower and ("in use" in error_lower or "already" in error_lower):
86
- # Try to extract port number
87
- import re
88
- port_match = re.search(r"port\s+(\d+)", error_lower)
89
- port = port_match.group(1) if port_match else "8000"
90
- return "port_in_use", {"port": port}
91
-
92
- # SSH detection
93
- if "ssh" in error_lower or "connection refused" in error_lower:
94
- return "ssh_failure", {"ip": "DROPLET_IP"}
95
-
96
- # Docker detection
97
- if "docker" in error_lower and "build" in error_lower:
98
- return "docker_build_failed", {}
99
-
100
- # Health check detection
101
- if "health" in error_lower and ("fail" in error_lower or "timeout" in error_lower):
102
- return "health_check_failed", {"port": "8000"}
103
-
104
- # Memory detection
105
- if "memory" in error_lower or "oom" in error_lower:
106
- return "out_of_memory", {}
107
-
108
- # Module not found
109
- if "modulenotfounderror" in error_lower or "no module named" in error_lower:
110
- import re
111
- module_match = re.search(r"no module named ['\"]([^'\"]+)['\"]", error_lower)
112
- package = module_match.group(1) if module_match else "PACKAGE_NAME"
113
- return "missing_dependency", {"package": package, "file": "pyproject.toml"}
114
-
115
- # Unknown
116
- return None, {}
1
+ """Human-friendly error messages with actionable solutions."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ ERROR_SOLUTIONS = {
9
+ "port_in_use": {
10
+ "message": "Port {port} is already in use on the droplet",
11
+ "solution": "Change the port in xenfra.yaml or stop the conflicting service",
12
+ "command": "ssh root@{{ip}} 'lsof -i :{port}' # Find process using port",
13
+ },
14
+ "missing_dependency": {
15
+ "message": "Missing dependency: {package}",
16
+ "solution": "Add {package} to dependencies in {file}",
17
+ "command": "uv add {package} # OR: echo '{package}' >> requirements.txt",
18
+ },
19
+ "ssh_failure": {
20
+ "message": "Cannot connect to droplet via SSH",
21
+ "solution": "Check firewall rules, wait for droplet boot, or verify SSH keys",
22
+ "command": "ssh -v root@{ip} # Verbose SSH for debugging",
23
+ },
24
+ "docker_build_failed": {
25
+ "message": "Docker build failed",
26
+ "solution": "Check Dockerfile syntax and base image availability",
27
+ "command": "docker build . --no-cache # Test locally",
28
+ },
29
+ "health_check_failed": {
30
+ "message": "Application failed health check",
31
+ "solution": "Ensure your app responds on port {port} at /health or /",
32
+ "command": "curl http://{{ip}}:{port}/health",
33
+ },
34
+ "out_of_memory": {
35
+ "message": "Container out of memory",
36
+ "solution": "Upgrade to a larger instance size in xenfra.yaml",
37
+ "command": "docker stats # Check memory usage",
38
+ },
39
+ }
40
+
41
+
42
+ def show_error_with_solution(error_type: str, **kwargs) -> None:
43
+ """
44
+ Display error with actionable solution.
45
+
46
+ Args:
47
+ error_type: Key from ERROR_SOLUTIONS
48
+ **kwargs: Template variables (port, ip, package, file, etc.)
49
+ """
50
+ error = ERROR_SOLUTIONS.get(error_type)
51
+
52
+ if not error:
53
+ # Fallback for unknown errors
54
+ console.print(f"[red]❌ Error: {error_type}[/red]")
55
+ return
56
+
57
+ # Format message with provided kwargs
58
+ try:
59
+ message = error["message"].format(**kwargs)
60
+ solution = error["solution"].format(**kwargs)
61
+ command = error.get("command", "").format(**kwargs)
62
+ except KeyError as e:
63
+ console.print(f"[red]❌ Error formatting message: missing {e}[/red]")
64
+ return
65
+
66
+ console.print()
67
+ console.print(f"[red]❌ {message}[/red]")
68
+ console.print(f"[yellow]💡 Solution: {solution}[/yellow]")
69
+
70
+ if command:
71
+ console.print(f"[dim]Try: {command}[/dim]")
72
+ console.print()
73
+
74
+
75
+ def detect_error_type(error_message: str) -> tuple[str, dict]:
76
+ """
77
+ Attempt to detect error type from message.
78
+
79
+ Returns:
80
+ (error_type, kwargs) for show_error_with_solution()
81
+ """
82
+ error_lower = error_message.lower()
83
+
84
+ # Port detection
85
+ if "port" in error_lower and ("in use" in error_lower or "already" in error_lower):
86
+ # Try to extract port number
87
+ import re
88
+ port_match = re.search(r"port\s+(\d+)", error_lower)
89
+ port = port_match.group(1) if port_match else "8000"
90
+ return "port_in_use", {"port": port}
91
+
92
+ # SSH detection
93
+ if "ssh" in error_lower or "connection refused" in error_lower:
94
+ return "ssh_failure", {"ip": "DROPLET_IP"}
95
+
96
+ # Docker detection
97
+ if "docker" in error_lower and "build" in error_lower:
98
+ return "docker_build_failed", {}
99
+
100
+ # Health check detection
101
+ if "health" in error_lower and ("fail" in error_lower or "timeout" in error_lower):
102
+ return "health_check_failed", {"port": "8000"}
103
+
104
+ # Memory detection
105
+ if "memory" in error_lower or "oom" in error_lower:
106
+ return "out_of_memory", {}
107
+
108
+ # Module not found
109
+ if "modulenotfounderror" in error_lower or "no module named" in error_lower:
110
+ import re
111
+ module_match = re.search(r"no module named ['\"]([^'\"]+)['\"]", error_lower)
112
+ package = module_match.group(1) if module_match else "PACKAGE_NAME"
113
+ return "missing_dependency", {"package": package, "file": "pyproject.toml"}
114
+
115
+ # Unknown
116
+ return None, {}