Resonance-Calc / core /validation_utils.py
SergeyO7's picture
Upload 25 files
790625d verified
import re
from datetime import datetime
from typing import Optional, Tuple
import html
from functools import wraps
import time
import gradio as gr
import logging
from core.logging_config import get_logger
# Initialize logger
logger = get_logger(__name__)
def validate_datetime_format(datetime_str: str) -> Tuple[bool, Optional[datetime], Optional[str]]:
"""
Validates datetime string format and returns parsed datetime object
Expected format: DD-MM-YYYY HH:MM:SS
"""
if not datetime_str or not isinstance(datetime_str, str):
return False, None, "Datetime string is empty or not a string"
# Check for valid format using regex
pattern = r'^(\d{2})-(\d{2})-(\d{4})\s(\d{2}):(\d{2}):(\d{2})$'
match = re.match(pattern, datetime_str.strip())
if not match:
return False, None, f"Datetime format invalid. Expected DD-MM-YYYY HH:MM:SS, got: {datetime_str}"
try:
day, month, year, hour, minute, second = map(int, match.groups())
# Validate date components
if not (1 <= day <= 31):
return False, None, f"Invalid day: {day}. Day must be between 1 and 31"
if not (1 <= month <= 12):
return False, None, f"Invalid month: {month}. Month must be between 1 and 12"
if year < 1900 or year > 2100:
return False, None, f"Invalid year: {year}. Year must be between 1900 and 2100"
if not (0 <= hour <= 23):
return False, None, f"Invalid hour: {hour}. Hour must be between 0 and 23"
if not (0 <= minute <= 59):
return False, None, f"Invalid minute: {minute}. Minute must be between 0 and 59"
if not (0 <= second <= 59):
return False, None, f"Invalid second: {second}. Second must be between 0 and 59"
# Try to create datetime object to catch invalid dates like 30th of February
dt = datetime(year, month, day, hour, minute, second)
return True, dt, None
except ValueError as e:
return False, None, f"Invalid date: {str(e)}"
except Exception as e:
return False, None, f"Error parsing datetime: {str(e)}"
def validate_city_name(city: str) -> Tuple[bool, Optional[str], Optional[str]]:
"""
Validates city name input to prevent injection attacks
"""
if not city or not isinstance(city, str):
return False, None, "City name is empty or not a string"
# Sanitize input by removing potentially dangerous characters
sanitized_city = html.escape(city.strip())
# Check for potentially dangerous patterns
dangerous_patterns = [
r'[<>"\']', # HTML/SQL injection characters
r'[;{}]', # Command injection characters
r'\$\(', # Command substitution
r'`', # Command execution
]
for pattern in dangerous_patterns:
if re.search(pattern, sanitized_city):
return False, None, f"Potentially dangerous characters detected in city name: {sanitized_city}"
# Additional check for length
if len(sanitized_city) > 100:
return False, None, f"City name too long: {len(sanitized_city)} characters. Maximum is 100"
# Check if it contains only alphanumeric characters, spaces, hyphens, commas, slashes, periods, and cyrillic
if not re.match(r'^[a-zA-Zа-яА-ЯёЁ0-9\s\-\/\.,]{2,100}$', sanitized_city):
return False, None, f"City name contains invalid characters: {sanitized_city}"
return True, sanitized_city, None
def validate_coordinates(lat: float, lon: float) -> Tuple[bool, Optional[str]]:
"""
Validates geographic coordinates
"""
if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)):
return False, "Coordinates must be numeric values"
if not (-90 <= lat <= 90):
return False, f"Latitude out of range: {lat}. Must be between -90 and 90"
if not (-180 <= lon <= 180):
return False, f"Longitude out of range: {lon}. Must be between -180 and 180"
return True, None
def sanitize_input(input_str: str) -> str:
"""
Sanitizes string input by removing potentially dangerous characters
"""
if not input_str or not isinstance(input_str, str):
return ""
# Remove potentially dangerous characters
sanitized = html.escape(input_str.strip())
# Remove any control characters
sanitized = ''.join(char for char in sanitized if ord(char) >= 32 or char in ['\n', '\t', '\r'])
return sanitized
def sanitize_inputs(datetime_str: str, city: str) -> tuple[bool, str]:
"""Validate and sanitize user inputs"""
# Validate datetime format
if not re.match(r'^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$', datetime_str):
return False, "Invalid datetime format"
# Validate city name (alphanumeric, spaces, hyphens, commas, slashes, periods, and cyrillic)
if not re.match(r'^[a-zA-Zа-яА-ЯёЁ0-9\s\-\/\.,]{2,100}$', city):
return False, "Invalid city name"
# Additional date validation
try:
day, month, year_time = datetime_str.split('-')
year, time = year_time.split(' ')
datetime(int(year), int(month), int(day))
except ValueError:
return False, "Invalid date"
return True, ""
class RateLimiter:
def __init__(self, max_calls: int, period: int):
self.max_calls = max_calls
self.period = period
self.calls = []
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove calls outside the current period
self.calls = [call for call in self.calls if now - call < self.period]
if len(self.calls) >= self.max_calls:
logger.warning(f"Rate limit exceeded for function {func.__name__}")
raise gr.Error("Rate limit exceeded. Please try again later.")
self.calls.append(now)
logger.debug(f"Function {func.__name__} called, current call count: {len(self.calls)}")
return func(*args, **kwargs)
return wrapper