""" DeduplicationService - Explicit deduplikáció a márka, technikai kód és jármű típus alapján. Integrálja a mapping_rules.py és mapping_dictionary.py fájlokat. """ import logging from typing import Optional, Dict, Any from sqlalchemy import select, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession from app.models.vehicle_definitions import VehicleModelDefinition from app.workers.vehicle.mapping_rules import SOURCE_MAPPINGS, unify_data logger = logging.getLogger(__name__) # Ha nincs mapping_dictionary, hozzunk létre egy egyszerű szinonima szótárt MAPPING_DICTIONARY = { "make_synonyms": { "BMW": ["BMW", "Bayerische Motoren Werke"], "MERCEDES": ["MERCEDES", "MERCEDES-BENZ", "MERCEDES BENZ"], "VOLKSWAGEN": ["VOLKSWAGEN", "VW"], "AUDI": ["AUDI"], "TOYOTA": ["TOYOTA"], "FORD": ["FORD"], # További márkák... }, "technical_code_synonyms": { # Példa: "1.8 TSI" -> ["1.8 TSI", "1.8TSI", "1.8 TSI 180"] }, "vehicle_class_synonyms": { "SUV": ["SUV", "SPORT UTILITY VEHICLE"], "SEDAN": ["SEDAN", "SALOON"], "HATCHBACK": ["HATCHBACK", "HATCH"], "COUPE": ["COUPE", "COUPÉ"], } } class DeduplicationService: """Szolgáltatás a duplikált járműmodell rekordok azonosítására és kezelésére.""" @staticmethod def normalize_make(make: str) -> str: """Normalizálja a márka nevet a szinonimák alapján.""" make_upper = make.strip().upper() for canonical, synonyms in MAPPING_DICTIONARY["make_synonyms"].items(): if make_upper in synonyms or make_upper == canonical: return canonical return make_upper @staticmethod def normalize_technical_code(technical_code: Optional[str]) -> str: """Normalizálja a technikai kódot (pl. motor kód).""" if not technical_code: return "" # Egyszerű whitespace és pont eltávolítás code = technical_code.strip().upper() # További normalizáció: eltávolítás speciális karakterek import re code = re.sub(r'[^A-Z0-9]', '', code) return code @staticmethod def normalize_vehicle_class(vehicle_class: Optional[str]) -> str: """Normalizálja a jármű osztályt.""" if not vehicle_class: return "" class_upper = vehicle_class.strip().upper() for canonical, synonyms in MAPPING_DICTIONARY["vehicle_class_synonyms"].items(): if class_upper in synonyms or class_upper == canonical: return canonical return class_upper @classmethod async def find_duplicate( cls, session: AsyncSession, make: str, technical_code: str, vehicle_class: str, exclude_id: Optional[int] = None ) -> Optional[VehicleModelDefinition]: """ Megkeresi, hogy létezik-e már ugyanilyen (normalizált) rekord a vehicle_model_definitions táblában. Args: session: SQLAlchemy async session make: márka (pl. "BMW") technical_code: technikai kód (pl. "N47") vehicle_class: jármű osztály (pl. "SEDAN") exclude_id: kizárni kívánt rekord ID (pl. frissítésnél) Returns: VehicleModelDefinition instance ha talált duplikátumot, egyébként None. """ norm_make = cls.normalize_make(make) norm_technical_code = cls.normalize_technical_code(technical_code) norm_vehicle_class = cls.normalize_vehicle_class(vehicle_class) # Keresés a normalizált értékek alapján stmt = select(VehicleModelDefinition).where( and_( VehicleModelDefinition.make.ilike(f"%{norm_make}%"), VehicleModelDefinition.technical_code.ilike(f"%{norm_technical_code}%"), VehicleModelDefinition.vehicle_class.ilike(f"%{norm_vehicle_class}%") ) ) if exclude_id: stmt = stmt.where(VehicleModelDefinition.id != exclude_id) result = await session.execute(stmt) duplicate = result.scalar_one_or_none() if duplicate: logger.info(f"Duplikátum találva: ID {duplicate.id} - {duplicate.make} {duplicate.technical_code} {duplicate.vehicle_class}") return duplicate @classmethod async def ensure_no_duplicate( cls, session: AsyncSession, make: str, technical_code: str, vehicle_class: str, exclude_id: Optional[int] = None ) -> bool: """ Ellenőrzi, hogy nincs-e duplikátum. Ha van, False-t ad vissza. """ duplicate = await cls.find_duplicate(session, make, technical_code, vehicle_class, exclude_id) return duplicate is None @classmethod async def deduplicate_and_merge( cls, session: AsyncSession, new_record: Dict[str, Any], source_name: str = "manual" ) -> Dict[str, Any]: """ Duplikáció ellenőrzése és esetleges merge logika. Ha talál duplikátumot, visszaadja a meglévő rekord adatait. Ha nem, visszaadja a normalizált új rekordot. Args: session: SQLAlchemy async session new_record: új rekord adatai (make, technical_code, vehicle_class, stb.) source_name: adatforrás neve a mapping_rules-hoz Returns: Dict with keys: - is_duplicate: bool - existing_id: int if duplicate else None - normalized_data: normalizált adatok """ # Normalizálás mapping_rules segítségével unified = unify_data(new_record, source_name) make = unified.get("normalized_make", new_record.get("make", "")) technical_code = new_record.get("technical_code", "") vehicle_class = new_record.get("vehicle_class", "") duplicate = await cls.find_duplicate(session, make, technical_code, vehicle_class) if duplicate: return { "is_duplicate": True, "existing_id": duplicate.id, "normalized_data": { "make": duplicate.make, "technical_code": duplicate.technical_code, "vehicle_class": duplicate.vehicle_class, } } # Nincs duplikátum, normalizált adatokkal tér vissza return { "is_duplicate": False, "existing_id": None, "normalized_data": { "make": cls.normalize_make(make), "technical_code": cls.normalize_technical_code(technical_code), "vehicle_class": cls.normalize_vehicle_class(vehicle_class), } }