mlenvdoctor 0.1.0__py3-none-any.whl → 0.1.2__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.
mlenvdoctor/utils.py CHANGED
@@ -5,11 +5,20 @@ import sys
5
5
  from pathlib import Path
6
6
  from typing import List, Optional, Tuple
7
7
 
8
+ import sys
9
+
8
10
  from rich.console import Console
9
11
  from rich.progress import Progress, SpinnerColumn, TextColumn
10
- from rich.text import Text
11
12
 
12
- console = Console()
13
+ from .exceptions import DiagnosticError
14
+ from .icons import icon_check, icon_cross, icon_info, icon_warning
15
+
16
+ # Configure console for Windows compatibility
17
+ if sys.platform == "win32":
18
+ # Use legacy Windows renderer if needed
19
+ console = Console(legacy_windows=True, force_terminal=True)
20
+ else:
21
+ console = Console()
13
22
 
14
23
 
15
24
  def run_command(
@@ -17,8 +26,29 @@ def run_command(
17
26
  capture_output: bool = True,
18
27
  check: bool = False,
19
28
  timeout: Optional[int] = 30,
20
- ) -> subprocess.CompletedProcess:
21
- """Run a shell command with error handling."""
29
+ ) -> subprocess.CompletedProcess[str]:
30
+ """
31
+ Run a shell command with error handling and input validation.
32
+
33
+ Args:
34
+ cmd: Command and arguments as a list
35
+ capture_output: Whether to capture stdout/stderr
36
+ check: Whether to raise on non-zero exit code
37
+ timeout: Command timeout in seconds
38
+
39
+ Returns:
40
+ CompletedProcess with command result
41
+
42
+ Raises:
43
+ DiagnosticError: For command execution errors
44
+ ConfigurationError: For invalid input
45
+ """
46
+ from .validators import sanitize_command, validate_timeout
47
+
48
+ # Validate and sanitize inputs
49
+ cmd = sanitize_command(cmd)
50
+ timeout = validate_timeout(timeout)
51
+
22
52
  try:
23
53
  result = subprocess.run(
24
54
  cmd,
@@ -28,29 +58,58 @@ def run_command(
28
58
  timeout=timeout,
29
59
  )
30
60
  return result
31
- except subprocess.TimeoutExpired:
32
- console.print(f"[red]Command timed out: {' '.join(cmd)}[/red]")
33
- raise
34
- except FileNotFoundError:
35
- console.print(f"[red]Command not found: {cmd[0]}[/red]")
36
- raise
61
+ except subprocess.TimeoutExpired as e:
62
+ error_msg = f"Command timed out after {timeout}s: {' '.join(cmd)}"
63
+ console.print(f"[red]{error_msg}[/red]")
64
+ raise DiagnosticError(
65
+ error_msg,
66
+ "Try increasing timeout or check if the command is hanging",
67
+ ) from e
68
+ except FileNotFoundError as e:
69
+ error_msg = f"Command not found: {cmd[0]}"
70
+ console.print(f"[red]{error_msg}[/red]")
71
+ raise DiagnosticError(
72
+ error_msg,
73
+ f"Install {cmd[0]} or ensure it's in your PATH",
74
+ ) from e
37
75
  except subprocess.CalledProcessError as e:
38
76
  if not check:
39
- return e # type: ignore
77
+ # Return the exception as if it were a result
78
+ # This maintains backward compatibility but is type-unsafe
79
+ return subprocess.CompletedProcess( # type: ignore[return-value]
80
+ cmd, e.returncode, e.stdout, e.stderr
81
+ )
40
82
  raise
41
83
 
42
84
 
43
85
  def check_command_exists(cmd: str) -> bool:
44
- """Check if a command exists in PATH."""
86
+ """
87
+ Check if a command exists in PATH.
88
+
89
+ Args:
90
+ cmd: Command name to check
91
+
92
+ Returns:
93
+ True if command exists and is executable, False otherwise
94
+ """
95
+ if not isinstance(cmd, str) or not cmd.strip():
96
+ return False
97
+
45
98
  try:
46
- subprocess.run(
47
- [cmd, "--version"] if cmd != "nvidia-smi" else [cmd],
99
+ # Use 'which' on Unix, 'where' on Windows
100
+ if sys.platform == "win32":
101
+ check_cmd = ["where", cmd]
102
+ else:
103
+ check_cmd = ["which", cmd]
104
+
105
+ result = subprocess.run(
106
+ check_cmd,
48
107
  capture_output=True,
49
108
  timeout=5,
50
109
  check=False,
51
110
  )
52
- return True
53
- except (FileNotFoundError, subprocess.TimeoutExpired):
111
+ return result.returncode == 0
112
+ except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
54
113
  return False
55
114
 
56
115
 
@@ -64,22 +123,22 @@ def get_home_config_dir() -> Path:
64
123
 
65
124
  def print_success(message: str) -> None:
66
125
  """Print a success message."""
67
- console.print(f"[green] {message}[/green]")
126
+ console.print(f"[green]{icon_check()} {message}[/green]")
68
127
 
69
128
 
70
129
  def print_error(message: str) -> None:
71
130
  """Print an error message."""
72
- console.print(f"[red] {message}[/red]")
131
+ console.print(f"[red]{icon_cross()} {message}[/red]")
73
132
 
74
133
 
75
134
  def print_warning(message: str) -> None:
76
135
  """Print a warning message."""
77
- console.print(f"[yellow]⚠️ {message}[/yellow]")
136
+ console.print(f"[yellow]{icon_warning()} {message}[/yellow]")
78
137
 
79
138
 
80
139
  def print_info(message: str) -> None:
81
140
  """Print an info message."""
82
- console.print(f"[blue]ℹ️ {message}[/blue]")
141
+ console.print(f"[blue]{icon_info()} {message}[/blue]")
83
142
 
84
143
 
85
144
  def with_spinner(message: str):
@@ -103,5 +162,3 @@ def format_size(size_bytes: int) -> str:
103
162
  def get_python_version() -> Tuple[int, int, int]:
104
163
  """Get Python version as tuple."""
105
164
  return sys.version_info[:3]
106
-
107
-
@@ -0,0 +1,217 @@
1
+ """Input validation and sanitization for ML Environment Doctor."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from .exceptions import ConfigurationError
8
+
9
+
10
+ def validate_model_name(model_name: str) -> str:
11
+ """
12
+ Validate and sanitize model name.
13
+
14
+ Args:
15
+ model_name: Model name to validate
16
+
17
+ Returns:
18
+ Sanitized model name
19
+
20
+ Raises:
21
+ ConfigurationError: If model name is invalid
22
+ """
23
+ if not model_name or not isinstance(model_name, str):
24
+ raise ConfigurationError(
25
+ "Model name must be a non-empty string",
26
+ "Use a valid model name like 'tinyllama', 'gpt2', or 'mistral-7b'",
27
+ )
28
+
29
+ # Remove whitespace
30
+ model_name = model_name.strip()
31
+
32
+ # Check for dangerous characters (basic sanitization)
33
+ if not re.match(r"^[a-zA-Z0-9._-]+$", model_name):
34
+ raise ConfigurationError(
35
+ f"Invalid model name: {model_name}",
36
+ "Model name can only contain letters, numbers, dots, underscores, and hyphens",
37
+ )
38
+
39
+ return model_name.lower()
40
+
41
+
42
+ def validate_file_path(file_path: Path, must_exist: bool = False, must_be_file: bool = False) -> Path:
43
+ """
44
+ Validate and sanitize file path.
45
+
46
+ Args:
47
+ file_path: Path to validate
48
+ must_exist: Whether the path must exist
49
+ must_be_file: Whether the path must be a file
50
+
51
+ Returns:
52
+ Resolved, absolute path
53
+
54
+ Raises:
55
+ ConfigurationError: If path is invalid
56
+ """
57
+ if not isinstance(file_path, (Path, str)):
58
+ raise ConfigurationError(
59
+ "File path must be a Path object or string",
60
+ "Use pathlib.Path or a valid string path",
61
+ )
62
+
63
+ path = Path(file_path).resolve()
64
+
65
+ # Check for path traversal attempts
66
+ if ".." in str(path):
67
+ # Resolve should handle this, but double-check
68
+ resolved = path.resolve()
69
+ if ".." in str(resolved):
70
+ raise ConfigurationError(
71
+ "Invalid path: contains '..'",
72
+ "Use absolute paths or relative paths without '..'",
73
+ )
74
+
75
+ if must_exist and not path.exists():
76
+ raise ConfigurationError(
77
+ f"Path does not exist: {path}",
78
+ "Ensure the file or directory exists",
79
+ )
80
+
81
+ if must_be_file and not path.is_file():
82
+ raise ConfigurationError(
83
+ f"Path is not a file: {path}",
84
+ "Provide a valid file path",
85
+ )
86
+
87
+ return path
88
+
89
+
90
+ def validate_log_level(level: str) -> str:
91
+ """
92
+ Validate logging level.
93
+
94
+ Args:
95
+ level: Log level to validate
96
+
97
+ Returns:
98
+ Validated log level
99
+
100
+ Raises:
101
+ ConfigurationError: If level is invalid
102
+ """
103
+ valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
104
+ level_upper = level.upper() if isinstance(level, str) else str(level).upper()
105
+
106
+ if level_upper not in valid_levels:
107
+ raise ConfigurationError(
108
+ f"Invalid log level: {level}",
109
+ f"Use one of: {', '.join(valid_levels)}",
110
+ )
111
+
112
+ return level_upper
113
+
114
+
115
+ def validate_stack_name(stack: str) -> str:
116
+ """
117
+ Validate ML stack name.
118
+
119
+ Args:
120
+ stack: Stack name to validate
121
+
122
+ Returns:
123
+ Validated stack name
124
+
125
+ Raises:
126
+ ConfigurationError: If stack is invalid
127
+ """
128
+ valid_stacks = {"trl-peft", "minimal"}
129
+ stack_lower = stack.lower() if isinstance(stack, str) else str(stack).lower()
130
+
131
+ if stack_lower not in valid_stacks:
132
+ raise ConfigurationError(
133
+ f"Invalid stack: {stack}",
134
+ f"Use one of: {', '.join(valid_stacks)}",
135
+ )
136
+
137
+ return stack_lower
138
+
139
+
140
+ def sanitize_command(cmd: list[str]) -> list[str]:
141
+ """
142
+ Sanitize command arguments to prevent injection.
143
+
144
+ Args:
145
+ cmd: Command and arguments list
146
+
147
+ Returns:
148
+ Sanitized command list
149
+
150
+ Raises:
151
+ ConfigurationError: If command contains dangerous patterns
152
+ """
153
+ if not isinstance(cmd, list) or not cmd:
154
+ raise ConfigurationError(
155
+ "Command must be a non-empty list",
156
+ "Provide command as a list of strings",
157
+ )
158
+
159
+ sanitized = []
160
+ for arg in cmd:
161
+ if not isinstance(arg, str):
162
+ raise ConfigurationError(
163
+ "All command arguments must be strings",
164
+ "Convert all arguments to strings",
165
+ )
166
+
167
+ # Check for command injection patterns
168
+ dangerous_patterns = [";", "&&", "||", "`", "$(", "<", ">", "|"]
169
+ for pattern in dangerous_patterns:
170
+ if pattern in arg:
171
+ raise ConfigurationError(
172
+ f"Dangerous pattern detected in command: {pattern}",
173
+ "Do not use shell operators in command arguments",
174
+ )
175
+
176
+ sanitized.append(arg)
177
+
178
+ return sanitized
179
+
180
+
181
+ def validate_timeout(timeout: Optional[int], min_timeout: int = 1, max_timeout: int = 3600) -> Optional[int]:
182
+ """
183
+ Validate timeout value.
184
+
185
+ Args:
186
+ timeout: Timeout in seconds
187
+ min_timeout: Minimum allowed timeout
188
+ max_timeout: Maximum allowed timeout
189
+
190
+ Returns:
191
+ Validated timeout
192
+
193
+ Raises:
194
+ ConfigurationError: If timeout is invalid
195
+ """
196
+ if timeout is None:
197
+ return None
198
+
199
+ if not isinstance(timeout, int):
200
+ raise ConfigurationError(
201
+ f"Timeout must be an integer, got {type(timeout)}",
202
+ "Provide timeout as an integer number of seconds",
203
+ )
204
+
205
+ if timeout < min_timeout:
206
+ raise ConfigurationError(
207
+ f"Timeout too small: {timeout}s (minimum: {min_timeout}s)",
208
+ f"Increase timeout to at least {min_timeout} seconds",
209
+ )
210
+
211
+ if timeout > max_timeout:
212
+ raise ConfigurationError(
213
+ f"Timeout too large: {timeout}s (maximum: {max_timeout}s)",
214
+ f"Decrease timeout to at most {max_timeout} seconds",
215
+ )
216
+
217
+ return timeout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlenvdoctor
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Diagnose & fix ML environments for LLM fine-tuning
5
5
  Author: ML Environment Doctor Contributors
6
6
  License: MIT
@@ -20,6 +20,7 @@ Requires-Python: >=3.8
20
20
  Requires-Dist: packaging>=23.0
21
21
  Requires-Dist: psutil>=5.9.0
22
22
  Requires-Dist: rich>=13.0.0
23
+ Requires-Dist: tomli>=2.0.0; python_version < '3.11'
23
24
  Requires-Dist: typer>=0.9.0
24
25
  Provides-Extra: dev
25
26
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -34,7 +35,7 @@ Description-Content-Type: text/markdown
34
35
 
35
36
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
36
37
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
- [![PyPI version](https://badge.fury.io/py/mlenvdoctor.svg)](https://badge.fury.io/py/mlenvdoctor)
38
+ [![PyPI](https://img.shields.io/pypi/v/mlenvdoctor.svg)]([https://pypi.org/project/mlenvdoctor/])
38
39
 
39
40
  > **Single command fixes 90% of "my torch.cuda.is_available() is False" issues.**
40
41
 
@@ -0,0 +1,21 @@
1
+ mlenvdoctor/__init__.py,sha256=igtJXQ-DiuG4_2BcfW64KPxvhCzSeuXUNUVqDzuLDI8,327
2
+ mlenvdoctor/cli.py,sha256=jvc1MWNciyRGedYCEKSCZeqXfX4wf6xe38Vx44k86xc,7145
3
+ mlenvdoctor/config.py,sha256=c7WUd8XUAo8nz-Ri4TIldc-_p3wiyDNqOsL-MhV9jbI,4770
4
+ mlenvdoctor/constants.py,sha256=ZoYf6-gqmw88CGENyYVur_hCmdcUm0IR5N9pUdOLjJY,1980
5
+ mlenvdoctor/diagnose.py,sha256=7lsAVOPeK2I3nmlzMKmtniyEh1UGv76bJqlqSpjvPL8,20553
6
+ mlenvdoctor/dockerize.py,sha256=AC8HX5sRkSFAM0O0caBnKW4HAdS49MVmMcsplKEDXI4,5562
7
+ mlenvdoctor/exceptions.py,sha256=8wzZE-In0zimXJ3omUA3YmFeklcseO53kqo0SpB58DM,1069
8
+ mlenvdoctor/export.py,sha256=CsuLpbpR2OVs-bud27K8Xv28KCI9veNPIWCsznyFmaw,8671
9
+ mlenvdoctor/fix.py,sha256=fXS4uxBN-FWRFKowmOsdPYI7bnY8jnYxG7UADpJ1hwc,8989
10
+ mlenvdoctor/gpu.py,sha256=sMFgtF4pt-dpOr6IDxvm6f0ChfmCf58E8mApIH6jvAs,6295
11
+ mlenvdoctor/icons.py,sha256=vu35SuBxlZu55rUqldgQfX4UeHeENt082ighZKnzHZY,2289
12
+ mlenvdoctor/logger.py,sha256=OKJQjcdOspARokBcIDyCri14gS_7MlrCt2B3znKc34Y,2377
13
+ mlenvdoctor/parallel.py,sha256=HVJmu8t4k2-XeoQPHlLmhInLOcXjUTiP0VM3THOkAmE,3630
14
+ mlenvdoctor/retry.py,sha256=ZH-KWe6BPNK1sUNFSK2uwdobZ_Z77fxMUfSrSpInt3c,2993
15
+ mlenvdoctor/utils.py,sha256=ehohh-iRLe2qkOMxj5v9yTWONf5gWSdY6CvfrRttTlg,4862
16
+ mlenvdoctor/validators.py,sha256=Kz1FcJM4Cym-S_z5vTocv0cxzKOEgvZIqv8C8c1gSzY,6109
17
+ mlenvdoctor-0.1.2.dist-info/METADATA,sha256=7Rlpv9kjMHQWPBsnZR1gbaUzocWNXNJ3wdGy5UXaNQU,8942
18
+ mlenvdoctor-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ mlenvdoctor-0.1.2.dist-info/entry_points.txt,sha256=Y-WH-ANeiTdECIaqi_EB3ZEf_kACkvsYBHnNhXsCI4k,52
20
+ mlenvdoctor-0.1.2.dist-info/licenses/LICENSE,sha256=rGHdyWGvGWYnEFlthqtB-RtRCTa7WaAOElom5qD-nHw,1114
21
+ mlenvdoctor-0.1.2.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- mlenvdoctor/__init__.py,sha256=hPyn5R1E9SDxyqhMb9AUsDw-TRxj9K2AWcadl5LmvqY,112
2
- mlenvdoctor/cli.py,sha256=DjF6UYC4QlrGSvbmG3wokX0urAWls9hu0FSIh-jZyYA,5305
3
- mlenvdoctor/diagnose.py,sha256=zk_E3UF2OlJr6ZFx3OjejyswgcZWX7qz1BHDXCB-vmk,16980
4
- mlenvdoctor/dockerize.py,sha256=q7afAUSkpL3RHSmbMZy2G9VZ7yRbus4xDADbSjIIuJM,5615
5
- mlenvdoctor/fix.py,sha256=V-mK30r2xSk-_3uuEHqH01iA9Vt8mjjlCisb4kL_A_Q,8959
6
- mlenvdoctor/gpu.py,sha256=PTS_dj6JaAoXKHQNzqgh8xOuZHomPoQxuLQMGWQXHqQ,6239
7
- mlenvdoctor/utils.py,sha256=fAZHgVX6iyOpcd4NU-oSyLjFMO1AXnfgOL5_KL_-0Po,2925
8
- mlenvdoctor-0.1.0.dist-info/METADATA,sha256=mZnlvzRuaaW9ydTZf5HsP4-wtDs7LUKUEde2xu_STmo,8889
9
- mlenvdoctor-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
- mlenvdoctor-0.1.0.dist-info/entry_points.txt,sha256=Y-WH-ANeiTdECIaqi_EB3ZEf_kACkvsYBHnNhXsCI4k,52
11
- mlenvdoctor-0.1.0.dist-info/licenses/LICENSE,sha256=rGHdyWGvGWYnEFlthqtB-RtRCTa7WaAOElom5qD-nHw,1114
12
- mlenvdoctor-0.1.0.dist-info/RECORD,,