Date: 2025-01-09 Status: ✅ Utility Ready - Migration In Progress
We've created a comprehensive validators utility (validators.py) that eliminates 150+ lines of duplicate validation code across the codebase.
Impact: Standardized validation with consistent error messages, type safety, and composable validators.
String validation patterns (50+ occurrences):
- Empty/None checks
- Length validation
- Pattern matching
String sanitization (151 occurrences):
.strip(),.lower(),.upper()- Alphanumeric filtering
File validation (30+ occurrences):
- Extension checking
- Size validation
- Filename safety
Collection validation (40+ occurrences):
- Non-empty lists
- Size limits
Entity/relation type validation (20+ occurrences):
- Membership in allowed sets
BEFORE (autobot-backend/api/auth.py:26-36 - 11 lines):
@validator("username")
def validate_username(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError("Username cannot be empty")
if len(v) > 50:
raise ValueError("Username too long")
# Basic sanitization
v = v.strip().lower()
if not v.replace("_", "").replace("-", "").replace(".", "").isalnum():
raise ValueError("Username contains invalid characters")
return vAFTER (2 lines - 9 lines saved):
from src.utils.validators import validate_username
@validator("username")
def validate_username(cls, v):
return validate_username(v, min_length=3, max_length=50)BEFORE (autobot-backend/api/auth.py:38-44 - 7 lines):
@validator("password")
def validate_password(cls, v):
if not v or len(v) < 1:
raise ValueError("Password cannot be empty")
if len(v) > 128:
raise ValueError("Password too long")
return vAFTER (2 lines - 5 lines saved):
from src.utils.validators import validate_password
@validator("password")
def validate_password(cls, v):
return validate_password(v, min_length=8, max_length=128)BEFORE (autobot-backend/api/memory.py:71-79 - 9 lines):
@validator("entity_type")
def validate_entity_type(cls, v):
valid_types = {
"conversation",
"bug_fix",
"feature",
"decision",
"task",
"user_preference",
}
if v not in valid_types:
raise ValueError(f"Invalid entity type: {v}")
return vAFTER (5 lines - 4 lines saved):
from src.utils.validators import validate_in_choices
@validator("entity_type")
def validate_entity_type(cls, v):
valid_types = {"conversation", "bug_fix", "feature", "decision", "task", "user_preference"}
return validate_in_choices(v, valid_types, "entity_type")BEFORE (8 lines):
def check_file_extension(filename: str) -> bool:
ext = Path(filename).suffix.lower()
allowed = {".txt", ".md", ".json", ".py"}
if ext not in allowed:
raise ValueError(f"Invalid extension: {ext}")
return TrueAFTER (2 lines - 6 lines saved):
from src.utils.validators import validate_file_extension
def check_file_extension(filename: str) -> bool:
return validate_file_extension(filename, {".txt", ".md", ".json", ".py"})BEFORE (autobot-backend/api/files.py:444-448 - 5 lines):
if len(content) > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File too large (maximum {MAX_FILE_SIZE} bytes)"
)AFTER (2 lines - 3 lines saved):
from src.utils.validators import validate_file_size
validate_file_size(len(content), "File", max_size=MAX_FILE_SIZE)from src.utils.validators import (
validate_non_empty_string,
validate_string_length,
validate_string_pattern
)
# Non-empty check
username = validate_non_empty_string(value, "Username")
# Length bounds
username = validate_string_length(username, "Username", min_length=3, max_length=50)
# Pattern matching
code = validate_string_pattern(value, r"^[A-Z0-9]{6}$", "Code")from src.utils.validators import sanitize_string, sanitize_alphanumeric
# Strip and lowercase
username = sanitize_string(value, strip=True, lowercase=True)
# Alphanumeric only
username = sanitize_alphanumeric(value, allowed_chars="_-.")from src.utils.validators import validate_non_empty_collection, validate_collection_size
# Non-empty
ids = validate_non_empty_collection(id_list, "File IDs")
# Size bounds
ids = validate_collection_size(id_list, "File IDs", min_size=1, max_size=100)from src.utils.validators import validate_in_choices
# Must be in allowed set
entity_type = validate_in_choices(value, {"task", "bug", "feature"}, "Entity Type")
# Case-insensitive
status = validate_in_choices(value, {"active", "inactive"}, "Status", case_sensitive=False)from src.utils.validators import (
validate_file_extension,
validate_filename_safe,
validate_file_size
)
# Extension check
filename = validate_file_extension(filename, {".txt", ".md", ".json"})
# Safe filename (no path traversal)
filename = validate_filename_safe(filename, max_length=255)
# Size check
validate_file_size(file_bytes, "Document", max_size=50*1024*1024)from src.utils.validators import validate_email, validate_url, validate_uuid
# Email format
email = validate_email(value, "Email")
# URL format
url = validate_url(value, "URL", require_https=True)
# UUID format
uuid = validate_uuid(value, "ID")from src.utils.validators import validate_username, validate_password
# Complete username validation (sanitize + validate + check length)
username = validate_username(value, min_length=3, max_length=50)
# Complete password validation
password = validate_password(value, min_length=8, max_length=128)- Add import:
from src.utils.validators import ... - Replace validation logic: Use appropriate validator function
- Remove duplicate code: Delete manual checks, length validation, error messages
- Keep custom logic: Only for business-specific validation
- Test validation: Verify error messages and edge cases
- Update tests: Match new error messages if needed
| File | Validators | Est. Lines Saved |
|---|---|---|
autobot-backend/api/auth.py |
2 validators | ~15 lines |
autobot-backend/api/memory.py |
2 validators | ~15 lines |
autobot-backend/api/conversation_files.py |
1 validator | ~5 lines |
autobot-backend/api/files.py- File validation (extension, size, filename)autobot-backend/api/conversation_files.py- File uploads
- All files using
.strip(),.lower(),.upper()(151 occurrences) - 37 files with string manipulation
Total Estimated Savings: 150+ lines across codebase
| Benefit | Before | After |
|---|---|---|
| Lines per Validation | 5-11 lines | 1-2 lines |
| Total Savings | N/A | 150+ lines |
| Error Messages | ✅ Standardized | |
| Reusability | ❌ Copy-paste | ✅ Composable |
| Type Safety | ✅ Type hints | |
| Testing | ✅ Centralized (58 tests) |
Problem: Need custom error message
Solution: Catch and re-raise with custom message
try:
validate_string_length(value, "Field", max_length=50)
except ValueError:
raise ValueError("Custom error message here")Problem: Need to chain multiple validations
Solution: Call validators sequentially
value = sanitize_string(value, strip=True, lowercase=True)
value = validate_non_empty_string(value, "Username")
value = validate_string_length(value, "Username", min_length=3, max_length=50)Or use composite validator:
value = validate_username(value, min_length=3, max_length=50)58 comprehensive tests in tests/test_validators.py covering:
- ✅ String validators (11 tests)
- ✅ String sanitizers (8 tests)
- ✅ Collection validators (6 tests)
- ✅ Choice validators (4 tests)
- ✅ File validators (10 tests)
- ✅ Format validators (9 tests)
- ✅ Composite validators (10 tests)
All tests passing - utility is functional ✅
- Start with Pydantic models - Highest impact, easiest migration
- File operations - File validation patterns
- String sanitization - Replace
.strip().lower()patterns - Custom validators - Use as building blocks
Migration Status: Ready to begin - utility tested and functional ✅