228 lines
11 KiB
Groff
Executable File
228 lines
11 KiB
Groff
Executable File
import asyncio
|
|
import httpx
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
from sqlalchemy import text, select
|
|
from app.database import AsyncSessionLocal
|
|
from app.models.vehicle_definitions import VehicleModelDefinition
|
|
|
|
# MB 2.0 Szigorú naplózás
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s [%(levelname)s] Robot-1-Hunter: %(message)s',
|
|
stream=sys.stdout
|
|
)
|
|
logger = logging.getLogger("Robot-1")
|
|
|
|
class CatalogHunter:
|
|
"""
|
|
Vehicle Robot 1.7.3: Mega-Hunter (Industrial Master Version)
|
|
Teljes körű RDW adatbányászat Variant/Version szintű granuláltsággal.
|
|
"""
|
|
RDW_MAIN = "https://opendata.rdw.nl/resource/m9d7-ebf2.json"
|
|
RDW_FUEL = "https://opendata.rdw.nl/resource/8ys7-d773.json"
|
|
RDW_ENGINE = "https://opendata.rdw.nl/resource/jh96-v4pq.json"
|
|
|
|
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
|
|
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
|
|
BATCH_SIZE = 50
|
|
|
|
@classmethod
|
|
def normalize(cls, text_val: str) -> str:
|
|
if not text_val: return ""
|
|
return re.sub(r'[^a-zA-Z0-9]', '', text_val).lower()
|
|
|
|
@classmethod
|
|
def parse_int(cls, value) -> int:
|
|
try:
|
|
if value is None or str(value).strip() == "": return 0
|
|
return int(float(value))
|
|
except (ValueError, TypeError): return 0
|
|
|
|
@classmethod
|
|
def parse_float(cls, value) -> float:
|
|
try:
|
|
if value is None or str(value).strip() == "": return 0.0
|
|
return float(value)
|
|
except (ValueError, TypeError): return 0.0
|
|
|
|
@classmethod
|
|
async def fetch_tech_details(cls, client, plate):
|
|
""" Extra technikai adatok (kW, Euro, CO2, Motorkód) párhuzamos lekérése. """
|
|
urls = {
|
|
"fuel": f"{cls.RDW_FUEL}?kenteken={plate}",
|
|
"engine": f"{cls.RDW_ENGINE}?kenteken={plate}"
|
|
}
|
|
results = {
|
|
"power_kw": 0,
|
|
"engine_code": None,
|
|
"euro_class": None,
|
|
"fuel_desc": "Unknown",
|
|
"co2": 0,
|
|
"consumption": 0.0
|
|
}
|
|
try:
|
|
resps = await asyncio.gather(*[client.get(u, headers=cls.HEADERS) for u in urls.values()])
|
|
|
|
# Üzemanyag és emisszió (8ys7-d773)
|
|
if resps[0].status_code == 200 and resps[0].json():
|
|
f = resps[0].json()[0]
|
|
p1 = cls.parse_int(f.get("netto_maximum_vermogen") or f.get("nettomaximumvermogen"))
|
|
p2 = cls.parse_int(f.get("nominaal_continu_maximum_vermogen") or f.get("nominaalcontinuvermogen"))
|
|
results.update({
|
|
"power_kw": max(p1, p2),
|
|
"fuel_desc": f.get("brandstof_omschrijving") or "Unknown",
|
|
"euro_class": f.get("euro_klasse") or f.get("uitlaatemissieniveau"),
|
|
"co2": cls.parse_int(f.get("co2_uitstoot_gecombineerd")),
|
|
"consumption": cls.parse_float(f.get("brandstofverbruik_gecombineerd"))
|
|
})
|
|
|
|
# Motorkód (jh96-v4pq)
|
|
if resps[1].status_code == 200 and resps[1].json():
|
|
results["engine_code"] = resps[1].json()[0].get("motorcode")
|
|
except Exception as e:
|
|
logger.error(f"❌ RDW-Extra hiba ({plate}): {e}")
|
|
return results
|
|
|
|
@classmethod
|
|
async def process_make_model(cls, db, task_id, make_name, model_name, v_class, priority):
|
|
clean_make = make_name.strip().upper()
|
|
clean_model = model_name.strip().upper()
|
|
logger.info(f"🎯 MEGA-VADÁSZAT INDUL: {clean_make} {clean_model}")
|
|
|
|
current_offset = 0
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
while True:
|
|
params = {
|
|
"merk": clean_make,
|
|
"handelsbenaming": clean_model,
|
|
"$limit": cls.BATCH_SIZE,
|
|
"$offset": current_offset,
|
|
"$order": "kenteken DESC"
|
|
}
|
|
try:
|
|
r = await client.get(cls.RDW_MAIN, params=params, headers=cls.HEADERS)
|
|
batch = r.json() if r.status_code == 200 else []
|
|
except Exception: break
|
|
|
|
if not batch:
|
|
await db.execute(text("UPDATE data.catalog_discovery SET status = 'processed' WHERE id = :id"), {"id": task_id})
|
|
await db.commit()
|
|
logger.info(f"🏁 {clean_make} {clean_model} minden variánsa feldolgozva.")
|
|
return
|
|
|
|
for item in batch:
|
|
try:
|
|
plate = item.get("kenteken")
|
|
if not plate: continue
|
|
|
|
# Alapadatok azonosítása
|
|
variant = item.get("variant")
|
|
version = item.get("uitvoering") # Az Execution/Version kód
|
|
ccm = cls.parse_int(item.get("cilinderinhoud"))
|
|
raw_model = str(item.get("handelsbenaming", "Unknown")).upper()
|
|
model_name_clean = raw_model.replace(clean_make, "").strip() or raw_model
|
|
norm_name = cls.normalize(model_name_clean)
|
|
|
|
# Extra technikai mélyfúrás (kW, Fuel, Engine)
|
|
tech = await cls.fetch_tech_details(client, plate)
|
|
|
|
# Ellenőrzés: Létezik-e már ez a pontos technikai variáns?
|
|
stmt = select(VehicleModelDefinition).where(
|
|
VehicleModelDefinition.make == clean_make,
|
|
VehicleModelDefinition.normalized_name == norm_name,
|
|
VehicleModelDefinition.engine_capacity == ccm,
|
|
VehicleModelDefinition.variant_code == variant,
|
|
VehicleModelDefinition.version_code == version,
|
|
VehicleModelDefinition.fuel_type == tech["fuel_desc"]
|
|
).limit(1)
|
|
|
|
existing = (await db.execute(stmt)).scalar_one_or_none()
|
|
|
|
if existing:
|
|
# Meglévő rekord frissítése a prioritással és hiányzó adatokkal
|
|
existing.priority_score = priority
|
|
if tech["power_kw"] > 0: existing.power_kw = tech["power_kw"]
|
|
if tech["engine_code"]: existing.engine_code = tech["engine_code"]
|
|
if tech["co2"] > 0: existing.co2_emissions_combined = tech["co2"]
|
|
else:
|
|
# ÚJ REKORD LÉTREHOZÁSA - MINDEN ADAT MEZŐVEL
|
|
db.add(VehicleModelDefinition(
|
|
make=clean_make,
|
|
marketing_name=model_name_clean,
|
|
normalized_name=norm_name,
|
|
variant_code=variant,
|
|
version_code=version,
|
|
type_approval_number=item.get("typegoedkeuringsnummer"),
|
|
technical_code=plate, # Kötelező mező!
|
|
engine_capacity=ccm,
|
|
power_kw=tech["power_kw"],
|
|
fuel_type=tech["fuel_desc"],
|
|
engine_code=tech["engine_code"],
|
|
# Fizikai méretek és súlyok (RDW Main-ből)
|
|
seats=cls.parse_int(item.get("aantal_zitplaatsen")),
|
|
doors=cls.parse_int(item.get("aantal_deuren")),
|
|
width=cls.parse_int(item.get("breedte")),
|
|
wheelbase=cls.parse_int(item.get("wielbasis")),
|
|
list_price=cls.parse_int(item.get("catalogusprijs")),
|
|
max_speed=cls.parse_int(item.get("maximale_constructiesnelheid")),
|
|
towing_weight_unbraked=cls.parse_int(item.get("maximum_massa_trekken_ongeremd")),
|
|
towing_weight_braked=cls.parse_int(item.get("maximum_trekken_massa_geremd")),
|
|
curb_weight=cls.parse_int(item.get("massa_ledig_voertuig")),
|
|
max_weight=cls.parse_int(item.get("technische_max_massa_voertuig") or item.get("toegestane_maximum_massa_voertuig")),
|
|
body_type=item.get("inrichting"),
|
|
# Emissziós és környezeti adatok (RDW Extra-ból)
|
|
co2_emissions_combined=tech["co2"],
|
|
fuel_consumption_combined=tech["consumption"],
|
|
euro_classification=tech["euro_class"],
|
|
cylinders=cls.parse_int(item.get("aantal_cilinders")),
|
|
# Meta adatok
|
|
vehicle_class=v_class,
|
|
priority_score=priority,
|
|
status="ACTIVE",
|
|
source="MEGA-HUNTER-v1.7.3"
|
|
))
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Hiba a sor feldolgozásakor ({plate}): {e}")
|
|
|
|
# Batch commit 50 soronként
|
|
await db.commit()
|
|
current_offset += len(batch)
|
|
|
|
# Biztonsági korlát: egy típusból ne szedjünk le többet, mint ami a variációkhoz kell
|
|
if current_offset >= 300: break
|
|
await asyncio.sleep(0.1)
|
|
|
|
@classmethod
|
|
async def run(cls):
|
|
logger.info("🤖 Mega-Hunter Robot v1.7.3 ONLINE")
|
|
while True:
|
|
try:
|
|
async with AsyncSessionLocal() as db:
|
|
# Legmagasabb prioritású modellek bekérése a Discovery listából
|
|
query = text("""
|
|
SELECT id, make, model, vehicle_class, priority_score
|
|
FROM data.catalog_discovery
|
|
WHERE status IN ('pending', 'processing')
|
|
ORDER BY priority_score DESC
|
|
LIMIT 1
|
|
""")
|
|
task = (await db.execute(query)).fetchone()
|
|
|
|
if task:
|
|
# Állapot rögzítése a "dupla munka" ellen
|
|
await db.execute(text("UPDATE data.catalog_discovery SET status = 'processing' WHERE id = :id"), {"id": task[0]})
|
|
await db.commit()
|
|
|
|
await cls.process_make_model(db, task[0], task[1], task[2], task[3], task[4])
|
|
else:
|
|
logger.info("😴 Nincs több feladat, pihenés 30 másodpercig...")
|
|
await asyncio.sleep(30)
|
|
except Exception as e:
|
|
logger.error(f"💀 Kritikus hiba a futtatás során: {e}")
|
|
await asyncio.sleep(10)
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(CatalogHunter.run()) |