198 lines
9.7 KiB
Python
198 lines
9.7 KiB
Python
import asyncio
|
|
import httpx
|
|
import logging
|
|
import json
|
|
import re
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, or_, text
|
|
from app.db.session import SessionLocal
|
|
from app.models.asset import AssetCatalog
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger("Robot1-Master-Fleet-DeepDive")
|
|
|
|
class CatalogScout:
|
|
"""
|
|
Robot 1: Univerzális Járműkatalógus Építő és Audit Robot.
|
|
Logika: EU-Elsődlegesség (CarQuery) -> US-Kiegészítés (NHTSA).
|
|
Kategóriák: Car, Motorcycle, Bus, Truck, Trailer, ATV, Marine, Aerial.
|
|
Szekvenciák:
|
|
1. Deep Dive (Motorvariánsok gyűjtése)
|
|
2. Audit (Hiányos adatok pótlása)
|
|
"""
|
|
|
|
CQ_URL = "https://www.carqueryapi.com/api/0.3/"
|
|
NHTSA_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles/GetModelsForMakeYear/make/"
|
|
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
"Accept": "application/json"
|
|
}
|
|
|
|
# --- KATEGÓRIA DEFINÍCIÓK (Szigorú flotta-szétválasztás) ---
|
|
MOTO_MAKES = ['ducati', 'ktm', 'triumph', 'aprilia', 'benelli', 'vespa', 'simson', 'mz', 'etz', 'jawa', 'husqvarna', 'gasgas', 'sherco']
|
|
MARINE_IDS = ['DF', 'DT', 'OUTBOARD', 'MARINE', 'JET SKI', 'SEA-DOO', 'WAVERUNNER', 'YACHT', 'BOAT']
|
|
AERIAL_IDS = ['CESSNA', 'PIPER', 'AIRBUS', 'BOEING', 'HELICOPTER', 'AIRCRAFT', 'BEECHCRAFT', 'EMBRAER', 'DRONE']
|
|
ATV_IDS = ['LT-', 'LTZ', 'LTR', 'KINGQUAD', 'QUAD', 'POLARIS', 'CAN-AM', 'MULE', 'RZR', 'ARCTIC CAT', 'UTV', 'SIDE-BY-SIDE']
|
|
|
|
# Versenygépek (Motorkerékpárként, üzemóra alapú szervizhez)
|
|
RACING_IDS = ['RM-Z', 'KX', 'CRF', 'YZ', 'SX-F', 'XC-W', 'RM125', 'RM250', 'CR125', 'CR250', 'MC450']
|
|
MOTO_KEYWORDS = ['CBR', 'GSX', 'YZF', 'NINJA', 'Z1000', 'DR-Z', 'MT-0', 'V-STROM', 'ADVENTURE', 'SCRAMBLER', 'CBF', 'VFR', 'HAYABUSA']
|
|
|
|
# Flotta kategóriák szétválasztása
|
|
BUS_KEYWORDS = ['BUS', 'COACH', 'INTERCITY', 'SHUTTLE', 'TRANSIT']
|
|
TRUCK_KEYWORDS = ['TRUCK', 'SEMI', 'TRACTOR', 'HAULER', 'ACTROS', 'MAN', 'SCANIA', 'IVECO', 'VOLVO FH', 'DAF', 'TGX', 'RENAULT T']
|
|
TRAILER_KEYWORDS = ['TRAILER', 'SEMITRAILER', 'PÓTKOCSI', 'UTÁNFUTÓ', 'SCHMITZ', 'KRONE', 'KÖGEL']
|
|
|
|
@classmethod
|
|
def identify_class(cls, make: str, model: str) -> str:
|
|
"""Kategória meghatározás flottakezelési szempontok alapján."""
|
|
m_full = f"{make} {model}".upper()
|
|
|
|
if any(x in m_full for x in cls.AERIAL_IDS): return "aerial"
|
|
if any(x in m_full for x in cls.MARINE_IDS): return "marine"
|
|
if any(x in m_full for x in cls.ATV_IDS): return "atv"
|
|
|
|
# Motorkerékpárok (Versenygépekkel együtt)
|
|
if any(x in m_full for x in cls.RACING_IDS) or make.lower() in cls.MOTO_MAKES:
|
|
return "motorcycle"
|
|
if any(x in m_full for x in cls.MOTO_KEYWORDS):
|
|
return "motorcycle"
|
|
|
|
# Flotta (Busz vs Teherautó vs Pótkocsi)
|
|
if any(x in m_full for x in cls.BUS_KEYWORDS): return "bus"
|
|
if any(x in m_full for x in cls.TRUCK_KEYWORDS): return "truck"
|
|
if any(x in m_full for x in cls.TRAILER_KEYWORDS): return "trailer"
|
|
|
|
return "car"
|
|
|
|
@classmethod
|
|
async def fetch_api(cls, url, params=None, is_cq=False):
|
|
"""API hívó JSONP tisztítással és sebességkorlátozással."""
|
|
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
|
|
try:
|
|
# 1.5s várakozás a Free API limitjei miatt
|
|
await asyncio.sleep(1.5)
|
|
resp = await client.get(url, params=params, timeout=35)
|
|
if resp.status_code != 200: return None
|
|
|
|
content = resp.text.strip()
|
|
if is_cq:
|
|
# Robusztusabb JSONP tisztítás regexszel
|
|
match = re.search(r'(\{.*\}|\[.*\])', content, re.DOTALL)
|
|
if match:
|
|
content = match.group(0)
|
|
elif "(" in content and ")" in content:
|
|
content = content[content.find("(") + 1 : content.rfind(")")]
|
|
|
|
return json.loads(content)
|
|
except Exception as e:
|
|
logger.error(f"❌ API hiba: {e} | URL: {url}")
|
|
return None
|
|
|
|
@classmethod
|
|
async def enrich_missing_data(cls):
|
|
"""
|
|
SEQUENCE 2: Audit Robot.
|
|
Keresi a hiányos technikai adatokat és próbálja dúsítani őket.
|
|
"""
|
|
logger.info("🔍 Audit szekvencia indítása (hiányos adatok keresése)...")
|
|
async with SessionLocal() as db:
|
|
# Keressük azokat a rekordokat, ahol hiányzik a köbcenti vagy a teljesítmény
|
|
stmt = select(AssetCatalog).where(
|
|
or_(
|
|
AssetCatalog.factory_data == text("'{}'::jsonb"),
|
|
AssetCatalog.engine_variant == 'Standard',
|
|
AssetCatalog.fuel_type == None
|
|
)
|
|
).limit(100) # Egyszerre csak 100-at nézünk
|
|
|
|
results = await db.execute(stmt)
|
|
incomplete_records = results.scalars().all()
|
|
|
|
for record in incomplete_records:
|
|
logger.info(f"🛠 Audit: {record.make} {record.model} ({record.year_from}) dúsítása...")
|
|
pass
|
|
|
|
@classmethod
|
|
async def run(cls):
|
|
logger.info("🤖 Robot 1: EU-Elsődlegességű Deep Dive szinkron indítása...")
|
|
|
|
# 2026-tól visszafelé haladunk (Modern flották prioritása)
|
|
for year in range(2026, 1989, -1):
|
|
logger.info(f"📅 Feldolgozás alatt: {year} évjárat")
|
|
|
|
makes_data = await cls.fetch_api(cls.CQ_URL, {"cmd": "getMakes", "year": year}, is_cq=True)
|
|
if not makes_data or "Makes" not in makes_data: continue
|
|
|
|
for make_entry in makes_data.get("Makes", []):
|
|
m_id = make_entry["make_id"]
|
|
m_display = make_entry["make_display"]
|
|
|
|
# MODELL GYŰJTÉS: EU + US fúzió
|
|
models_to_fetch = set()
|
|
|
|
# 🇪🇺 EU Forrás
|
|
cq_models = await cls.fetch_api(cls.CQ_URL, {"cmd": "getModels", "make": m_id, "year": year}, is_cq=True)
|
|
if cq_models and cq_models.get("Models"):
|
|
for m in cq_models["Models"]: models_to_fetch.add(m["model_name"])
|
|
|
|
# 🇺🇸 US Forrás kiegészítés
|
|
n_data = await cls.fetch_api(f"{cls.NHTSA_BASE}{m_display}/modelyear/{year}?format=json")
|
|
if n_data and n_data.get("Results"):
|
|
for r in n_data["Results"]: models_to_fetch.add(r["Model_Name"])
|
|
|
|
async with SessionLocal() as db:
|
|
for model_name in models_to_fetch:
|
|
# DEEP DIVE: Motorvariánsok (Trims) lekérése
|
|
trims_data = await cls.fetch_api(cls.CQ_URL, {
|
|
"cmd": "getTrims", "make": m_id, "model": model_name, "year": year
|
|
}, is_cq=True)
|
|
|
|
found_trims = trims_data.get("Trims", []) if trims_data else []
|
|
|
|
# Ha nincs trim adat, egy standard sor mindenképpen kell
|
|
if not found_trims:
|
|
found_trims = [{"model_trim": "Standard", "model_engine_fuel": None}]
|
|
|
|
for t in found_trims:
|
|
variant = t.get("model_trim") or "Standard"
|
|
fuel = t.get("model_engine_fuel") or "Unknown"
|
|
v_class = cls.identify_class(m_display, model_name)
|
|
|
|
# Szigorú duplikáció-ellenőrzés (UniqueConstraint alapú lekérdezés)
|
|
stmt = select(AssetCatalog).where(
|
|
AssetCatalog.make == m_display,
|
|
AssetCatalog.model == model_name,
|
|
AssetCatalog.year_from == year,
|
|
AssetCatalog.engine_variant == variant,
|
|
AssetCatalog.fuel_type == fuel
|
|
)
|
|
result = await db.execute(stmt)
|
|
if not result.scalars().first():
|
|
db.add(AssetCatalog(
|
|
make=m_display,
|
|
model=model_name,
|
|
year_from=year,
|
|
engine_variant=variant,
|
|
fuel_type=fuel,
|
|
vehicle_class=v_class,
|
|
factory_data={
|
|
"cc": t.get("model_engine_cc"),
|
|
"hp": t.get("model_engine_power_ps"),
|
|
"cylinders": t.get("model_engine_cyl"),
|
|
"transmission": t.get("model_transmission_type"),
|
|
"source": "master_v7_deep_dive",
|
|
"sync_date": str(func.now())
|
|
}
|
|
))
|
|
|
|
# JAVÍTÁS: Márkánkénti véglegesítés az adatbázisban a session-ön belül
|
|
await db.commit()
|
|
logger.info(f"✅ {m_display} ({year}) összes variánsa rögzítve.")
|
|
|
|
# SEQUENCE 2: Miután végeztünk a fő listával, nézzük meg a hiányosakat
|
|
await cls.enrich_missing_data()
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(CatalogScout.run()) |