winsweb's picture
adding model
39be5b4
Raw History Blame Contribute Delete
4.41 kB
"""
Data validation utilities for telecom data
"""
from datetime import datetime
from typing import Optional, Tuple, Any
import logging
logger = logging.getLogger(__name__)
class DataValidator:
"""Validate and clean telecom data"""
@staticmethod
def validate_msisdn(msisdn: str) -> bool:
"""Validate MSISDN format"""
if not msisdn:
return False
msisdn_str = str(msisdn).strip()
# Allow service names
if msisdn_str in ['MTN', 'MOBILEMONEY', 'MTN-DATA', 'MTNXTRATIME', 'EVD']:
return True
# Ghana format: starts with 233 and 12 digits
return msisdn_str.startswith('233') and len(msisdn_str) == 12
@staticmethod
def derive_msisdn(call_direction: str, calling_no: str, called_no: str) -> str:
"""
Derive the subscriber's MSISDN from the leg numbers when the CSV
does not provide an explicit msisdn column.
Outgoing legs -> subscriber is the caller; Incoming legs -> the callee.
Falls back to whichever side is a valid Ghana MSISDN.
"""
direction = (call_direction or '').upper()
calling = (str(calling_no).strip() if calling_no is not None else '')
called = (str(called_no).strip() if called_no is not None else '')
if 'OUT' in direction or 'MO' in direction:
primary, secondary = calling, called
elif 'IN' in direction or 'MT' in direction:
primary, secondary = called, calling
else:
primary, secondary = calling, called
if DataValidator.validate_msisdn(primary):
return primary
if DataValidator.validate_msisdn(secondary):
return secondary
return ''
@staticmethod
def validate_duration(duration: Any) -> float:
"""Validate and clean duration"""
if duration is None or duration == '':
return 0.0
try:
duration_float = float(duration)
return max(0.0, duration_float)
except (ValueError, TypeError):
return 0.0
@staticmethod
def validate_coordinates(lat: Any, lon: Any) -> Tuple[Optional[float], Optional[float]]:
"""Validate latitude and longitude"""
try:
lat_float = float(lat) if lat not in (None, '', '0.0') else None
lon_float = float(lon) if lon not in (None, '', '0.0') else None
if lat_float is not None and not (-90 <= lat_float <= 90):
lat_float = None
if lon_float is not None and not (-180 <= lon_float <= 180):
lon_float = None
return lat_float, lon_float
except (ValueError, TypeError):
return None, None
@staticmethod
def parse_datetime(date_val: Any, time_val: Any) -> Optional[datetime]:
"""Parse datetime from date and time strings or objects"""
try:
if not date_val or not time_val or date_val == 'NULL' or time_val == 'NULL':
return None
# If they are already date/time objects
from datetime import date, time
if isinstance(date_val, date) and isinstance(time_val, time):
return datetime.combine(date_val, time_val)
# If they are strings
return datetime.strptime(f"{str(date_val)} {str(time_val)}", "%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError) as e:
logger.debug(f"Failed to parse datetime: {date_val} {time_val} - {e}")
return None
@staticmethod
def is_night_call(time_val: Any, night_start: int = 23, night_end: int = 5) -> bool:
"""Check if call occurred at night"""
try:
if not time_val or time_val == 'NULL':
return False
from datetime import time
if isinstance(time_val, time):
hour = time_val.hour
else:
hour = int(str(time_val).split(':')[0])
return hour >= night_start or hour < night_end
except (ValueError, IndexError, AttributeError):
return False
@staticmethod
def is_service_number(calling_no: str, service_numbers: list) -> bool:
"""Check if number is a service number"""
return calling_no in service_numbers