ai-coding-assistant 0.5.0__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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""File path validation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
|
|
7
|
+
from coding_assistant.exceptions.validation import (
|
|
8
|
+
ValidationError,
|
|
9
|
+
FileNotFoundError,
|
|
10
|
+
PathSecurityError
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileValidator:
|
|
15
|
+
"""
|
|
16
|
+
Validates file paths for safety and existence.
|
|
17
|
+
|
|
18
|
+
Prevents path traversal and other security issues.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Dangerous path patterns
|
|
22
|
+
DANGEROUS_PATTERNS = [
|
|
23
|
+
'..', # Path traversal
|
|
24
|
+
'~/', # Home directory
|
|
25
|
+
'/etc', # System config
|
|
26
|
+
'/sys', # System files
|
|
27
|
+
'/proc', # Process info
|
|
28
|
+
'/dev', # Devices
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def validate_file_path(
|
|
33
|
+
path: str,
|
|
34
|
+
must_exist: bool = False,
|
|
35
|
+
must_be_file: bool = False,
|
|
36
|
+
must_be_dir: bool = False,
|
|
37
|
+
allowed_extensions: Optional[List[str]] = None,
|
|
38
|
+
base_dir: Optional[Path] = None
|
|
39
|
+
) -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Validate file path for safety and correctness.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path: File path to validate
|
|
45
|
+
must_exist: Whether file must exist
|
|
46
|
+
must_be_file: Whether path must be a file (not directory)
|
|
47
|
+
must_be_dir: Whether path must be a directory
|
|
48
|
+
allowed_extensions: List of allowed file extensions (e.g., ['.py', '.js'])
|
|
49
|
+
base_dir: Base directory to restrict paths to
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Validated Path object
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValidationError: If path is invalid
|
|
56
|
+
FileNotFoundError: If file doesn't exist when required
|
|
57
|
+
PathSecurityError: If path is potentially dangerous
|
|
58
|
+
"""
|
|
59
|
+
if not path:
|
|
60
|
+
raise ValidationError(
|
|
61
|
+
"file path",
|
|
62
|
+
"Path cannot be empty"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Convert to Path
|
|
66
|
+
try:
|
|
67
|
+
file_path = Path(path)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise ValidationError(
|
|
70
|
+
"file path",
|
|
71
|
+
f"Invalid path format: {e}",
|
|
72
|
+
value=path
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Security checks
|
|
76
|
+
FileValidator._check_path_security(file_path)
|
|
77
|
+
|
|
78
|
+
# Resolve to absolute path
|
|
79
|
+
try:
|
|
80
|
+
file_path = file_path.resolve()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
"file path",
|
|
84
|
+
f"Cannot resolve path: {e}",
|
|
85
|
+
value=path
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Check if within base directory
|
|
89
|
+
if base_dir:
|
|
90
|
+
base_dir = base_dir.resolve()
|
|
91
|
+
try:
|
|
92
|
+
file_path.relative_to(base_dir)
|
|
93
|
+
except ValueError:
|
|
94
|
+
raise PathSecurityError(
|
|
95
|
+
file_path,
|
|
96
|
+
f"Path is outside allowed directory: {base_dir}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Check existence
|
|
100
|
+
if must_exist and not file_path.exists():
|
|
101
|
+
raise FileNotFoundError(file_path)
|
|
102
|
+
|
|
103
|
+
# Check file vs directory
|
|
104
|
+
if file_path.exists():
|
|
105
|
+
if must_be_file and not file_path.is_file():
|
|
106
|
+
raise ValidationError(
|
|
107
|
+
"file path",
|
|
108
|
+
f"Path is not a file: {file_path}",
|
|
109
|
+
value=str(file_path)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if must_be_dir and not file_path.is_dir():
|
|
113
|
+
raise ValidationError(
|
|
114
|
+
"file path",
|
|
115
|
+
f"Path is not a directory: {file_path}",
|
|
116
|
+
value=str(file_path)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Check extension
|
|
120
|
+
if allowed_extensions and file_path.suffix.lower() not in allowed_extensions:
|
|
121
|
+
raise ValidationError(
|
|
122
|
+
"file path",
|
|
123
|
+
f"File type not allowed. Allowed: {', '.join(allowed_extensions)}",
|
|
124
|
+
value=str(file_path),
|
|
125
|
+
suggestion=f"Use a file with one of these extensions: {', '.join(allowed_extensions)}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return file_path
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _check_path_security(file_path: Path) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Check path for security issues.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
file_path: Path to check
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
PathSecurityError: If path is dangerous
|
|
140
|
+
"""
|
|
141
|
+
path_str = str(file_path)
|
|
142
|
+
|
|
143
|
+
# Check for dangerous patterns
|
|
144
|
+
for pattern in FileValidator.DANGEROUS_PATTERNS:
|
|
145
|
+
if pattern in path_str:
|
|
146
|
+
# Allow .. only in resolved paths that don't escape
|
|
147
|
+
if pattern == '..' and not path_str.startswith('..'):
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
raise PathSecurityError(
|
|
151
|
+
file_path,
|
|
152
|
+
f"Path contains dangerous pattern: {pattern}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Check for absolute paths to system directories
|
|
156
|
+
if file_path.is_absolute():
|
|
157
|
+
parts = file_path.parts
|
|
158
|
+
if len(parts) > 1 and parts[1] in ['etc', 'sys', 'proc', 'dev', 'boot']:
|
|
159
|
+
raise PathSecurityError(
|
|
160
|
+
file_path,
|
|
161
|
+
"Access to system directories not allowed"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def validate_project_path(path: str) -> Path:
|
|
166
|
+
"""
|
|
167
|
+
Validate project root path.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
path: Project path
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Validated Path object
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValidationError: If path is invalid
|
|
177
|
+
"""
|
|
178
|
+
project_path = FileValidator.validate_file_path(
|
|
179
|
+
path,
|
|
180
|
+
must_exist=True,
|
|
181
|
+
must_be_dir=True
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Check if it looks like a code project
|
|
185
|
+
# (has common files like .git, src/, package.json, etc.)
|
|
186
|
+
indicators = [
|
|
187
|
+
'.git',
|
|
188
|
+
'src',
|
|
189
|
+
'lib',
|
|
190
|
+
'package.json',
|
|
191
|
+
'requirements.txt',
|
|
192
|
+
'pyproject.toml',
|
|
193
|
+
'Cargo.toml',
|
|
194
|
+
'go.mod',
|
|
195
|
+
'.gitignore'
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
has_indicator = any(
|
|
199
|
+
(project_path / indicator).exists()
|
|
200
|
+
for indicator in indicators
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not has_indicator:
|
|
204
|
+
# Just a warning, not an error
|
|
205
|
+
pass # Could add warning log
|
|
206
|
+
|
|
207
|
+
return project_path
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def validate_output_path(
|
|
211
|
+
path: str,
|
|
212
|
+
allow_overwrite: bool = False
|
|
213
|
+
) -> Path:
|
|
214
|
+
"""
|
|
215
|
+
Validate output file path.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
path: Output file path
|
|
219
|
+
allow_overwrite: Whether to allow overwriting existing files
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Validated Path object
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValidationError: If path is invalid or file exists
|
|
226
|
+
"""
|
|
227
|
+
output_path = FileValidator.validate_file_path(path)
|
|
228
|
+
|
|
229
|
+
# Check if file exists
|
|
230
|
+
if output_path.exists() and not allow_overwrite:
|
|
231
|
+
raise ValidationError(
|
|
232
|
+
"output path",
|
|
233
|
+
"File already exists",
|
|
234
|
+
value=str(output_path),
|
|
235
|
+
suggestion="Use a different path or enable overwrite"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Check if parent directory exists and is writable
|
|
239
|
+
parent = output_path.parent
|
|
240
|
+
if not parent.exists():
|
|
241
|
+
raise ValidationError(
|
|
242
|
+
"output path",
|
|
243
|
+
f"Parent directory does not exist: {parent}",
|
|
244
|
+
value=str(output_path),
|
|
245
|
+
suggestion=f"Create directory first: mkdir -p {parent}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not os.access(parent, os.W_OK):
|
|
249
|
+
raise ValidationError(
|
|
250
|
+
"output path",
|
|
251
|
+
f"Parent directory is not writable: {parent}",
|
|
252
|
+
value=str(output_path)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return output_path
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def is_code_file(file_path: Path) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Check if file is a code file.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
file_path: File path to check
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if file appears to be code
|
|
267
|
+
"""
|
|
268
|
+
code_extensions = {
|
|
269
|
+
'.py', '.js', '.ts', '.jsx', '.tsx',
|
|
270
|
+
'.java', '.c', '.cpp', '.h', '.hpp',
|
|
271
|
+
'.cs', '.go', '.rs', '.rb', '.php',
|
|
272
|
+
'.swift', '.kt', '.scala', '.R',
|
|
273
|
+
'.sql', '.sh', '.bash', '.zsh',
|
|
274
|
+
'.html', '.css', '.scss', '.less',
|
|
275
|
+
'.json', '.yaml', '.yml', '.toml',
|
|
276
|
+
'.xml', '.md', '.rst'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return file_path.suffix.lower() in code_extensions
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def get_relative_path(
|
|
283
|
+
file_path: Path,
|
|
284
|
+
base_path: Path
|
|
285
|
+
) -> Path:
|
|
286
|
+
"""
|
|
287
|
+
Get relative path from base.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
file_path: File path
|
|
291
|
+
base_path: Base path
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Relative path
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValidationError: If file_path is not under base_path
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
return file_path.relative_to(base_path)
|
|
301
|
+
except ValueError:
|
|
302
|
+
raise ValidationError(
|
|
303
|
+
"file path",
|
|
304
|
+
f"Path {file_path} is not under {base_path}"
|
|
305
|
+
)
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Input validation utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
|
|
7
|
+
from coding_assistant.exceptions.validation import (
|
|
8
|
+
ValidationError,
|
|
9
|
+
InvalidQueryError,
|
|
10
|
+
InvalidParameterError,
|
|
11
|
+
EmptyInputError
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InputValidator:
|
|
16
|
+
"""
|
|
17
|
+
Validates user inputs for safety and correctness.
|
|
18
|
+
|
|
19
|
+
Provides methods for validating queries, parameters, and other inputs.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Constants
|
|
23
|
+
MIN_QUERY_LENGTH = 3
|
|
24
|
+
MAX_QUERY_LENGTH = 500
|
|
25
|
+
MAX_FILE_PATH_LENGTH = 4096
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def validate_query(
|
|
29
|
+
query: str,
|
|
30
|
+
min_length: Optional[int] = None,
|
|
31
|
+
max_length: Optional[int] = None
|
|
32
|
+
) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Validate search/ask query.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
query: User query
|
|
38
|
+
min_length: Minimum query length (default: 3)
|
|
39
|
+
max_length: Maximum query length (default: 500)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Cleaned query string
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
InvalidQueryError: If query is invalid
|
|
46
|
+
EmptyInputError: If query is empty
|
|
47
|
+
"""
|
|
48
|
+
min_length = min_length or InputValidator.MIN_QUERY_LENGTH
|
|
49
|
+
max_length = max_length or InputValidator.MAX_QUERY_LENGTH
|
|
50
|
+
|
|
51
|
+
# Check for None or empty
|
|
52
|
+
if query is None:
|
|
53
|
+
raise EmptyInputError("query")
|
|
54
|
+
|
|
55
|
+
# Strip whitespace
|
|
56
|
+
query = query.strip()
|
|
57
|
+
|
|
58
|
+
if not query:
|
|
59
|
+
raise EmptyInputError("query")
|
|
60
|
+
|
|
61
|
+
# Check length
|
|
62
|
+
if len(query) < min_length:
|
|
63
|
+
raise InvalidQueryError(
|
|
64
|
+
query,
|
|
65
|
+
f"Query too short (minimum {min_length} characters)"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if len(query) > max_length:
|
|
69
|
+
raise InvalidQueryError(
|
|
70
|
+
query,
|
|
71
|
+
f"Query too long (maximum {max_length} characters)"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Check for suspicious patterns (injection attempts)
|
|
75
|
+
suspicious_patterns = [
|
|
76
|
+
r'<script', # XSS
|
|
77
|
+
r'javascript:', # XSS
|
|
78
|
+
r'\$\{', # Template injection
|
|
79
|
+
r'`[^`]*`', # Command substitution
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for pattern in suspicious_patterns:
|
|
83
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
84
|
+
raise InvalidQueryError(
|
|
85
|
+
query,
|
|
86
|
+
"Query contains suspicious patterns"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return query
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def validate_positive_int(
|
|
93
|
+
value: any,
|
|
94
|
+
name: str,
|
|
95
|
+
min_value: int = 1,
|
|
96
|
+
max_value: Optional[int] = None
|
|
97
|
+
) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Validate positive integer parameter.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
value: Value to validate
|
|
103
|
+
name: Parameter name
|
|
104
|
+
min_value: Minimum value (default: 1)
|
|
105
|
+
max_value: Maximum value (optional)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Validated integer
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
InvalidParameterError: If value is invalid
|
|
112
|
+
"""
|
|
113
|
+
# Check type
|
|
114
|
+
if not isinstance(value, int):
|
|
115
|
+
raise InvalidParameterError(
|
|
116
|
+
name,
|
|
117
|
+
value,
|
|
118
|
+
"positive integer"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Check positive
|
|
122
|
+
if value < min_value:
|
|
123
|
+
raise InvalidParameterError(
|
|
124
|
+
name,
|
|
125
|
+
value,
|
|
126
|
+
f"positive integer >= {min_value}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Check maximum
|
|
130
|
+
if max_value is not None and value > max_value:
|
|
131
|
+
raise InvalidParameterError(
|
|
132
|
+
name,
|
|
133
|
+
value,
|
|
134
|
+
f"positive integer <= {max_value}",
|
|
135
|
+
suggestion=f"Use a value between {min_value} and {max_value}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def validate_float_range(
|
|
142
|
+
value: any,
|
|
143
|
+
name: str,
|
|
144
|
+
min_value: float = 0.0,
|
|
145
|
+
max_value: float = 1.0
|
|
146
|
+
) -> float:
|
|
147
|
+
"""
|
|
148
|
+
Validate float in range.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
value: Value to validate
|
|
152
|
+
name: Parameter name
|
|
153
|
+
min_value: Minimum value
|
|
154
|
+
max_value: Maximum value
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Validated float
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
InvalidParameterError: If value is invalid
|
|
161
|
+
"""
|
|
162
|
+
# Check type
|
|
163
|
+
if not isinstance(value, (int, float)):
|
|
164
|
+
raise InvalidParameterError(
|
|
165
|
+
name,
|
|
166
|
+
value,
|
|
167
|
+
f"number between {min_value} and {max_value}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
value = float(value)
|
|
171
|
+
|
|
172
|
+
# Check range
|
|
173
|
+
if value < min_value or value > max_value:
|
|
174
|
+
raise InvalidParameterError(
|
|
175
|
+
name,
|
|
176
|
+
value,
|
|
177
|
+
f"number between {min_value} and {max_value}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def validate_choice(
|
|
184
|
+
value: str,
|
|
185
|
+
name: str,
|
|
186
|
+
choices: List[str],
|
|
187
|
+
case_sensitive: bool = False
|
|
188
|
+
) -> str:
|
|
189
|
+
"""
|
|
190
|
+
Validate value is in list of choices.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
value: Value to validate
|
|
194
|
+
name: Parameter name
|
|
195
|
+
choices: List of valid choices
|
|
196
|
+
case_sensitive: Whether to enforce case sensitivity
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Validated choice
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
InvalidParameterError: If value not in choices
|
|
203
|
+
"""
|
|
204
|
+
if not value:
|
|
205
|
+
raise EmptyInputError(name)
|
|
206
|
+
|
|
207
|
+
# Normalize case if not case-sensitive
|
|
208
|
+
if not case_sensitive:
|
|
209
|
+
value_lower = value.lower()
|
|
210
|
+
choices_map = {c.lower(): c for c in choices}
|
|
211
|
+
|
|
212
|
+
if value_lower in choices_map:
|
|
213
|
+
return choices_map[value_lower]
|
|
214
|
+
else:
|
|
215
|
+
if value in choices:
|
|
216
|
+
return value
|
|
217
|
+
|
|
218
|
+
# Not found
|
|
219
|
+
raise InvalidParameterError(
|
|
220
|
+
name,
|
|
221
|
+
value,
|
|
222
|
+
f"one of: {', '.join(choices)}",
|
|
223
|
+
suggestion=f"Choose from: {', '.join(choices)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def validate_boolean(value: any, name: str) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Validate boolean parameter.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
value: Value to validate
|
|
233
|
+
name: Parameter name
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Boolean value
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
InvalidParameterError: If value is not boolean-like
|
|
240
|
+
"""
|
|
241
|
+
if isinstance(value, bool):
|
|
242
|
+
return value
|
|
243
|
+
|
|
244
|
+
if isinstance(value, str):
|
|
245
|
+
value_lower = value.lower()
|
|
246
|
+
|
|
247
|
+
if value_lower in ('true', 'yes', '1', 'on', 'y'):
|
|
248
|
+
return True
|
|
249
|
+
elif value_lower in ('false', 'no', '0', 'off', 'n'):
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
raise InvalidParameterError(
|
|
253
|
+
name,
|
|
254
|
+
value,
|
|
255
|
+
"boolean (true/false)",
|
|
256
|
+
suggestion="Use true/false, yes/no, or 1/0"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def validate_non_empty(value: str, name: str) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Validate string is not empty.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
value: Value to validate
|
|
266
|
+
name: Field name
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Non-empty string
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
EmptyInputError: If value is empty
|
|
273
|
+
"""
|
|
274
|
+
if not value or not value.strip():
|
|
275
|
+
raise EmptyInputError(name)
|
|
276
|
+
|
|
277
|
+
return value.strip()
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def validate_pattern(
|
|
281
|
+
value: str,
|
|
282
|
+
name: str,
|
|
283
|
+
pattern: str,
|
|
284
|
+
description: str
|
|
285
|
+
) -> str:
|
|
286
|
+
"""
|
|
287
|
+
Validate string matches regex pattern.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
value: Value to validate
|
|
291
|
+
name: Field name
|
|
292
|
+
pattern: Regex pattern
|
|
293
|
+
description: Pattern description for error message
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Validated string
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
ValidationError: If value doesn't match pattern
|
|
300
|
+
"""
|
|
301
|
+
if not re.match(pattern, value):
|
|
302
|
+
raise ValidationError(
|
|
303
|
+
name,
|
|
304
|
+
f"Does not match expected format: {description}",
|
|
305
|
+
value=value
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return value
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def sanitize_string(value: str, max_length: Optional[int] = None) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Sanitize string input.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
value: String to sanitize
|
|
317
|
+
max_length: Maximum length (optional)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Sanitized string
|
|
321
|
+
"""
|
|
322
|
+
if not value:
|
|
323
|
+
return ""
|
|
324
|
+
|
|
325
|
+
# Strip whitespace
|
|
326
|
+
value = value.strip()
|
|
327
|
+
|
|
328
|
+
# Remove null bytes
|
|
329
|
+
value = value.replace('\x00', '')
|
|
330
|
+
|
|
331
|
+
# Truncate if needed
|
|
332
|
+
if max_length and len(value) > max_length:
|
|
333
|
+
value = value[:max_length]
|
|
334
|
+
|
|
335
|
+
return value
|