feat: Robot ecosystem v1.2.6 - Google Search RAG & Master-Merge logic stabilized
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric
|
||||
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric, Index
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
@@ -41,10 +41,11 @@ class ModelFeatureMap(Base):
|
||||
value = Column(String(100))
|
||||
|
||||
class VehicleModelDefinition(Base):
|
||||
"""MDM Master rekordok"""
|
||||
"""MDM Master rekordok - Kibővítve Deduplikációs és Évjárat mezőkkel (v1.2.5)"""
|
||||
__tablename__ = "vehicle_model_definitions"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
|
||||
Index('idx_vmd_lookup', 'make', 'technical_code'),
|
||||
{"schema": "data"}
|
||||
)
|
||||
|
||||
@@ -58,28 +59,46 @@ class VehicleModelDefinition(Base):
|
||||
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
|
||||
vehicle_class = Column(String(50))
|
||||
|
||||
# --- LOGISZTIKAI ÉS TECHNIKAI FIX OSZLOPOK (v1.9) ---
|
||||
# --- ÚJ MEZŐK AZ INTELLIGENS ÖSSZEFÉSÜLÉSHEZ ---
|
||||
# Ha ez a rekord egy duplikátum, itt tároljuk, melyik az eredeti (Master) rekord
|
||||
parent_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
|
||||
|
||||
# Gyártási intervallum meghatározása
|
||||
year_from = Column(Integer, nullable=True, index=True)
|
||||
year_to = Column(Integer, nullable=True, index=True)
|
||||
|
||||
# Alternatív elnevezések kereshetőséghez (pl. ["Tracer 9", "MT-09 Tracer"])
|
||||
synonyms = Column(JSON, server_default=text("'[]'::jsonb"))
|
||||
# -----------------------------------------------
|
||||
|
||||
# --- LOGISZTIKAI ÉS TECHNIKAI FIX OSZLOPOK ---
|
||||
engine_capacity = Column(Integer, index=True)
|
||||
power_kw = Column(Integer, index=True)
|
||||
max_weight_kg = Column(Integer, index=True) # Össztömeg
|
||||
max_weight_kg = Column(Integer, index=True)
|
||||
|
||||
axle_count = Column(Integer) # Tengelyek száma (Teher/Busz)
|
||||
payload_capacity_kg = Column(Integer) # Teherbírás
|
||||
cargo_volume_m3 = Column(Numeric(10, 2)) # Raktér térfogat
|
||||
cargo_length_mm = Column(Integer) # Raktér méretek
|
||||
axle_count = Column(Integer)
|
||||
payload_capacity_kg = Column(Integer)
|
||||
cargo_volume_m3 = Column(Numeric(10, 2))
|
||||
cargo_length_mm = Column(Integer)
|
||||
cargo_width_mm = Column(Integer)
|
||||
cargo_height_mm = Column(Integer)
|
||||
# --------------------------------------------------
|
||||
# ----------------------------------------------
|
||||
|
||||
specifications = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
features_json = Column(JSON, server_default=text("'{}'::jsonb")) # Összesített gyorseléréshez
|
||||
features_json = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
status = Column(String(20), server_default="unverified")
|
||||
status = Column(String(20), server_default="unverified") # unverified, ai_enriched, duplicate, manual_check
|
||||
is_master = Column(Boolean, default=False)
|
||||
source = Column(String(50))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
v_type_rel = relationship("VehicleType", back_populates="definitions")
|
||||
|
||||
# Önmagára hivatkozó kapcsolat a duplikációk kezeléséhez
|
||||
master_record = relationship("VehicleModelDefinition", remote_side=[id], backref="merged_variants")
|
||||
|
||||
# Meglévő kapcsolatok megtartása
|
||||
variants = relationship("AssetCatalog", back_populates="master_definition")
|
||||
@@ -1,72 +1,111 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import google.generativeai as genai
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
# Konfiguráció a .env-ből
|
||||
"""
|
||||
AI Service v1.2.5 - Final Integrated Edition
|
||||
- Robot 2: Technikai dúsítás (Search + Regex JSON parsing)
|
||||
- Robot 3: OCR (Controlled JSON generation)
|
||||
"""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
if api_key:
|
||||
genai.configure(api_key=api_key)
|
||||
|
||||
# 1.5 Flash a legjobb ár/érték/sebesség arányú multimodális modell
|
||||
model = genai.GenerativeModel('gemini-1.5-flash')
|
||||
client = genai.Client(api_key=api_key) if api_key else None
|
||||
PRIMARY_MODEL = "gemini-2.0-flash"
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 1.0
|
||||
except Exception: return 1.0
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Technikai dúsítás és névtisztítás (pl. Yamaha 4HN)."""
|
||||
"""Robot 2: Adatbányászat Google Search segítségével."""
|
||||
if not cls.client: return None
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
search_tool = types.Tool(google_search=types.GoogleSearch())
|
||||
|
||||
prompt = f"""
|
||||
Rendszer: Technikai gépjárműszakértő vagy.
|
||||
Feladat: Tisztítsd meg a '{make} {raw_model}' ({v_type}) adatot.
|
||||
Kimenet: Kizárólag JSON, magyarázat nélkül.
|
||||
Formátum:
|
||||
KERESS RÁ az interneten: {make} {raw_model} ({v_type}) pontos gyári modellkódja és technikai adatai.
|
||||
Adj választ szigorúan csak egy JSON blokkban:
|
||||
{{
|
||||
"marketing_name": "Tiszta modellnév",
|
||||
"technical_code": "Modellkód/Generáció",
|
||||
"marketing_name": "tiszta név",
|
||||
"synonyms": ["név1", "név2"],
|
||||
"technical_code": "gyári kód",
|
||||
"year_from": int,
|
||||
"year_to": int_vagy_null,
|
||||
"ccm": int,
|
||||
"kw": int,
|
||||
"maintenance": {{
|
||||
"oil_type": "pl. 10W-40",
|
||||
"oil_qty": float,
|
||||
"spark_plug": "típus",
|
||||
"coolant": "típus"
|
||||
}}
|
||||
"maintenance": {{ "oil_type": "string", "oil_qty": float, "spark_plug": "string", "coolant": "string" }}
|
||||
}}
|
||||
FONTOS: A 'technical_code' NEM lehet üres. Ha nem találod, adj 'N/A' értéket!
|
||||
"""
|
||||
|
||||
# Search tool használata esetén a response_mime_type tilos!
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi járműtechnikai adatbányász vagy. Csak tiszta JSON-t válaszolsz markdown kódblokk nélkül.",
|
||||
tools=[search_tool],
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.model.generate_content(prompt)
|
||||
# A Gemini néha ```json ... ``` blokkba teszi, ezt le kell tisztítani
|
||||
json_text = response.text.replace("```json", "").replace("```", "").strip()
|
||||
return json.loads(json_text)
|
||||
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
|
||||
text = response.text
|
||||
# Tisztítás: ha az AI mégis tenne bele markdown jeleket
|
||||
clean_json = re.sub(r'```json\s*|```', '', text).strip()
|
||||
res_json = json.loads(clean_json)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI Dúsítás hiba: {e}")
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: AI OCR - Forgalmi, Személyi, Számla, KM-óra elemzés."""
|
||||
"""Robot 3: OCR funkció - Forgalmi, Személyi, Számla, Odometer."""
|
||||
if not cls.client: return None
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompts = {
|
||||
"identity": "Olvasd le az okmányról: vezetéknév, keresztnév, okmányszám, lejárati idő, születési dátum.",
|
||||
"vehicle_reg": "Olvasd le a forgalmiból: rendszám, alvázszám (VIN), gyártmány, típus, kw, ccm, együttes tömeg, műszaki érvényesség.",
|
||||
"invoice": "Olvasd le a számláról: eladó neve/adószáma, vevő neve, bruttó összeg, dátum, tételek (alkatrész/munkadíj).",
|
||||
"odometer": "Olvasd le a képen látható műszerfalról a kilométeróra vagy üzemóra állását. Csak a számot add vissza."
|
||||
"identity": "Személyes okmány adatok (név, szám, lejárat).",
|
||||
"vehicle_reg": "Forgalmi adatok (rendszám, alvázszám, kW, ccm).",
|
||||
"invoice": "Számla adatok (partner, végösszeg, dátum).",
|
||||
"odometer": "Csak a kilométeróra állása számként."
|
||||
}
|
||||
|
||||
prompt = f"Rendszer: Profi OCR és dokumentum-elemző vagy. {prompts.get(doc_type, 'Elemezd a képet.')} Válaszolj tiszta JSON formátumban."
|
||||
|
||||
|
||||
# Itt maradhat a response_mime_type, mert nem használunk Search-öt
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
# A Gemini közvetlenül tud fogadni bytes adatot (képként)
|
||||
contents = [
|
||||
prompt,
|
||||
{"mime_type": "image/jpeg", "data": image_data}
|
||||
]
|
||||
response = cls.model.generate_content(contents)
|
||||
json_text = response.text.replace("```json", "").replace("```", "").strip()
|
||||
return json.loads(json_text)
|
||||
response = cls.client.models.generate_content(
|
||||
model=cls.PRIMARY_MODEL,
|
||||
contents=[
|
||||
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, 'OCR')}",
|
||||
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
|
||||
],
|
||||
config=config
|
||||
)
|
||||
res_json = json.loads(response.text)
|
||||
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
|
||||
logger.error(f"❌ OCR hiba: {e}")
|
||||
return None
|
||||
116
backend/app/services/ai_service.py.bak
Normal file
116
backend/app/services/ai_service.py.bak
Normal file
@@ -0,0 +1,116 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import SystemParameter
|
||||
|
||||
logger = logging.getLogger("AI-Service")
|
||||
|
||||
class AIService:
|
||||
"""
|
||||
AI Service v1.2.4 - Production Ready
|
||||
- Robot 2 (Technical Enrichment) & Robot 3 (OCR)
|
||||
- Fix: JSON response cleaning and array-to-dict transformation.
|
||||
"""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
client = genai.Client(api_key=api_key) if api_key else None
|
||||
PRIMARY_MODEL = "gemini-2.0-flash"
|
||||
|
||||
@classmethod
|
||||
async def get_config_delay(cls) -> float:
|
||||
"""Lekéri az adminisztrálható késleltetést az adatbázisból."""
|
||||
try:
|
||||
async with SessionLocal() as db:
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return float(param.value) if param else 1.0
|
||||
except Exception:
|
||||
return 1.0
|
||||
|
||||
@classmethod
|
||||
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 2: Gépjármű technikai adatok dúsítása."""
|
||||
if not cls.client:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompt = f"""
|
||||
Jármű: {make} {raw_model} ({v_type}).
|
||||
Adj technikai adatokat JSON formátumban.
|
||||
FONTOS: A 'technical_code' mező NEM lehet üres. Ha nem tudod a gyári kódot, adj 'N/A' értéket!
|
||||
|
||||
Várt struktúra:
|
||||
{{
|
||||
"marketing_name": "tiszta marketing név",
|
||||
"technical_code": "gyári kód vagy N/A",
|
||||
"ccm": egész szám,
|
||||
"kw": egész szám,
|
||||
"maintenance": {{
|
||||
"oil_type": "viszkozitás",
|
||||
"oil_qty": tizedes tört literben,
|
||||
"spark_plug": "gyertya típus",
|
||||
"coolant": "hűtőfolyadék"
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi gépjárműtechnikus vagy. Kizárólag tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json",
|
||||
temperature=0.1
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
|
||||
res_json = json.loads(response.text)
|
||||
|
||||
if isinstance(res_json, list) and len(res_json) > 0:
|
||||
res_json = res_json[0]
|
||||
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
||||
"""Robot 3: Multimodális OCR elemzés (Képbeolvasás)."""
|
||||
if not cls.client:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(await cls.get_config_delay())
|
||||
|
||||
prompts = {
|
||||
"identity": "Személyes okmány adatok.",
|
||||
"vehicle_reg": "Rendszám, alvázszám, technikai adatok.",
|
||||
"invoice": "Számla adatok, összegek, dátumok.",
|
||||
"odometer": "Csak a kilométeróra állása számként."
|
||||
}
|
||||
|
||||
config = types.GenerateContentConfig(
|
||||
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
|
||||
response_mime_type="application/json"
|
||||
)
|
||||
|
||||
try:
|
||||
response = cls.client.models.generate_content(
|
||||
model=cls.PRIMARY_MODEL,
|
||||
contents=[
|
||||
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, '')}",
|
||||
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
|
||||
],
|
||||
config=config
|
||||
)
|
||||
res_json = json.loads(response.text)
|
||||
if isinstance(res_json, list) and len(res_json) > 0:
|
||||
res_json = res_json[0]
|
||||
return res_json if isinstance(res_json, dict) else None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
|
||||
return None
|
||||
@@ -3,133 +3,113 @@ import httpx
|
||||
import logging
|
||||
import os
|
||||
import datetime
|
||||
import json
|
||||
from sqlalchemy import text, select, update
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||
from app.models.audit import ProcessLog
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.email_manager import EmailManager # Feltételezve, hogy létezik
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("Robot-v1.1.0-Master-Enricher")
|
||||
logger = logging.getLogger("Robot-Bulk-Master")
|
||||
|
||||
class TechEnricher:
|
||||
"""
|
||||
Master Enricher v1.1.0 - Hybrid RDW & AI Clean Edition
|
||||
- Cél: vehicle_model_definitions (Master) tábla tisztítása és dúsítása.
|
||||
- Megtartja a v1.0.4 RDW logikát, de kiegészíti AI-al a zajos adatokhoz (pl. Yamaha 4HN).
|
||||
"""
|
||||
|
||||
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
|
||||
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
|
||||
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
|
||||
|
||||
@classmethod
|
||||
def clean_num(cls, v):
|
||||
try: return int(float(v)) if v else None
|
||||
except: return None
|
||||
|
||||
@classmethod
|
||||
async def fetch_rdw_tech_data(cls, make, model):
|
||||
"""A v1.0.4-es RDW kereső logika."""
|
||||
clean_model = str(model).upper().replace(str(make).upper(), "").strip()
|
||||
if len(clean_model) < 2: return None
|
||||
|
||||
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
|
||||
params = {"merk": make.upper(), "handelsbenaming": str(model).strip().upper(), "$limit": 1}
|
||||
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
|
||||
try:
|
||||
await asyncio.sleep(1.1) # RDW Rate limit védelem
|
||||
resp = await client.get(cls.API_URL, params=params, timeout=20)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return data[0] if data else None
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ RDW API Hiba: {e}")
|
||||
return None
|
||||
resp = await client.get(cls.API_URL, params=params, timeout=15)
|
||||
return resp.json()[0] if resp.status_code == 200 and resp.json() else None
|
||||
except: return None
|
||||
|
||||
@classmethod
|
||||
async def run(cls):
|
||||
logger.info("🚀 Master Enricher v1.1.0 INDUL...")
|
||||
start_time = datetime.datetime.now()
|
||||
stats = {"processed": 0, "failed": 0, "cleaned": []}
|
||||
|
||||
async with SessionLocal() as db:
|
||||
# Csak azokat a Master rekordokat nézzük, amik még nincsenek hitelesítve
|
||||
stmt = select(VehicleModelDefinition).where(
|
||||
VehicleModelDefinition.status == "unverified"
|
||||
).limit(30) # Kisebb batch a biztonság érdekében
|
||||
|
||||
res = await db.execute(stmt)
|
||||
masters = res.scalars().all()
|
||||
|
||||
if not masters:
|
||||
logger.info("😴 Nincs dúsításra váró adat.")
|
||||
return
|
||||
|
||||
for master in masters:
|
||||
try:
|
||||
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name}")
|
||||
|
||||
# 1. Lépés: RDW adatok lekérése (v1.0.4 logika)
|
||||
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
|
||||
|
||||
# 2. Lépés: AI segítség kérése, ha az RDW nem elég vagy a név 'zajos' (pl. 4HN)
|
||||
# Ha a névben gyanús kódok vannak, az AI tisztítja meg
|
||||
if not rdw_data or "(" in master.marketing_name or len(master.marketing_name) < 5:
|
||||
ai_data = await AIService.get_clean_vehicle_data(
|
||||
master.make, master.marketing_name, master.vehicle_type
|
||||
)
|
||||
if ai_data:
|
||||
old_name = master.marketing_name
|
||||
master.marketing_name = ai_data.get("marketing_name", old_name)
|
||||
master.technical_code = ai_data.get("technical_code", master.technical_code)
|
||||
master.engine_capacity = ai_data.get("ccm", master.engine_capacity)
|
||||
master.power_kw = ai_data.get("kw", master.power_kw)
|
||||
master.specifications = ai_data.get("maintenance", {})
|
||||
stats["cleaned"].append(f"{old_name} -> {master.marketing_name}")
|
||||
|
||||
# Ha volt RDW adatunk, de az AI nem írta felül, töltsük be az RDW-t
|
||||
if rdw_data and master.status == "unverified":
|
||||
master.power_kw = cls.clean_num(rdw_data.get("netto_maximum_vermogen_kw"))
|
||||
master.engine_capacity = cls.clean_num(rdw_data.get("cilinderinhoud"))
|
||||
master.axle_count = cls.clean_num(rdw_data.get("aantal_assen"))
|
||||
|
||||
master.status = "ai_enriched"
|
||||
stats["processed"] += 1
|
||||
await db.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Hiba a(z) {master.id} rekordnál: {e}")
|
||||
stats["failed"] += 1
|
||||
await db.rollback()
|
||||
|
||||
# 3. JELENTÉS MENTÉSE ÉS EMAIL KÜLDÉS
|
||||
end_time = datetime.datetime.now()
|
||||
new_log = ProcessLog(
|
||||
process_name="Master-Enricher",
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
items_processed=stats["processed"],
|
||||
items_failed=stats["failed"],
|
||||
details=stats
|
||||
)
|
||||
db.add(new_log)
|
||||
await db.commit()
|
||||
|
||||
# Email küldés (Dummy hívás a meglévő EmailManager-hez)
|
||||
await cls.send_report_email(stats)
|
||||
|
||||
@classmethod
|
||||
async def send_report_email(cls, stats):
|
||||
report_body = f"Reggeli Robot Jelentés - {datetime.date.today()}\n\n"
|
||||
report_body += f"Sikeresen feldolgozva: {stats['processed']}\n"
|
||||
report_body += f"Hibák: {stats['failed']}\n\n"
|
||||
report_body += "Tisztított nevek:\n" + "\n".join(stats['cleaned'])
|
||||
logger.info("🚀 Master-Merge Robot FOLYAMATOS ÜZEMMÓD INDUL...")
|
||||
|
||||
logger.info("📧 Email jelentés elküldve az adminnak.")
|
||||
# EmailManager.send_admin_notification("Robot Report", report_body)
|
||||
while True: # Folyamatos ciklus, amíg el nem fogy az adat
|
||||
async with SessionLocal() as main_db:
|
||||
stmt = select(VehicleModelDefinition.id).where(
|
||||
VehicleModelDefinition.status == "unverified"
|
||||
).limit(50) # Egyszerre 50 ID-t foglalunk le
|
||||
res = await main_db.execute(stmt)
|
||||
ids = res.scalars().all()
|
||||
|
||||
if not ids:
|
||||
logger.info("🏁 Minden rekord feldolgozva. A robot megáll.")
|
||||
break
|
||||
|
||||
logger.info(f"📦 Új csomag indítása: {len(ids)} rekord.")
|
||||
|
||||
for m_id in ids:
|
||||
async with SessionLocal() as db:
|
||||
try:
|
||||
current = await db.get(VehicleModelDefinition, m_id)
|
||||
if not current: continue
|
||||
|
||||
logger.info(f"🧪 Feldolgozás: {current.make} {current.marketing_name} (ID: {m_id})")
|
||||
|
||||
rdw_data = await cls.fetch_rdw_tech_data(current.make, current.marketing_name)
|
||||
if rdw_data:
|
||||
current.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or current.engine_capacity
|
||||
current.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or current.power_kw
|
||||
|
||||
ai_data = await AIService.get_clean_vehicle_data(current.make, current.marketing_name, current.vehicle_type)
|
||||
|
||||
if ai_data:
|
||||
tech_code = ai_data.get("technical_code") or "N/A"
|
||||
new_ccm = ai_data.get("ccm") or current.engine_capacity
|
||||
|
||||
master_record = None
|
||||
if tech_code and tech_code != "N/A":
|
||||
stmt_master = select(VehicleModelDefinition).where(and_(
|
||||
VehicleModelDefinition.make == current.make,
|
||||
VehicleModelDefinition.technical_code == tech_code,
|
||||
VehicleModelDefinition.engine_capacity == new_ccm,
|
||||
VehicleModelDefinition.status == 'ai_enriched',
|
||||
VehicleModelDefinition.id != m_id
|
||||
))
|
||||
master_record = (await db.execute(stmt_master)).scalar_one_or_none()
|
||||
|
||||
if master_record:
|
||||
logger.info(f"🔗 Merge: ID:{m_id} -> Master ID:{master_record.id}")
|
||||
syns = set(master_record.synonyms or [])
|
||||
syns.update(ai_data.get("synonyms", []))
|
||||
syns.add(current.marketing_name)
|
||||
master_record.synonyms = list(syns)
|
||||
current.status = "duplicate"
|
||||
current.parent_id = master_record.id
|
||||
else:
|
||||
current.technical_code = tech_code if tech_code != "N/A" else f"N/A-{m_id}"
|
||||
current.marketing_name = ai_data.get("marketing_name", current.marketing_name)
|
||||
current.engine_capacity = new_ccm
|
||||
current.power_kw = ai_data.get("kw") or current.power_kw
|
||||
current.year_from = ai_data.get("year_from")
|
||||
current.year_to = ai_data.get("year_to")
|
||||
current.synonyms = ai_data.get("synonyms", [])
|
||||
|
||||
if ai_data.get("maintenance"):
|
||||
old_spec = current.specifications or {}
|
||||
old_spec.update(ai_data.get("maintenance"))
|
||||
current.specifications = old_spec
|
||||
|
||||
current.status = "ai_enriched"
|
||||
else:
|
||||
if not current.technical_code:
|
||||
current.technical_code = f"UNKNOWN-{m_id}"
|
||||
|
||||
current.updated_at = datetime.datetime.now()
|
||||
await db.commit()
|
||||
logger.info(f"✅ Mentve (ID: {m_id})")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Hiba ID:{m_id}: {e}")
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(TechEnricher.run())
|
||||
102
backend/app/workers/technical_enricher.py.bak
Normal file
102
backend/app/workers/technical_enricher.py.bak
Normal file
@@ -0,0 +1,102 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
import logging
|
||||
import os # <--- EZ HIÁNYZOTT!
|
||||
import datetime
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.vehicle_definitions import VehicleModelDefinition
|
||||
from app.services.ai_service import AIService
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("Robot-v1.2.4-Fixed")
|
||||
|
||||
class TechEnricher:
|
||||
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
|
||||
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
|
||||
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
|
||||
|
||||
@classmethod
|
||||
async def fetch_rdw_tech_data(cls, make, model):
|
||||
"""Hibatűrő RDW lekérdezés tisztított paraméterekkel."""
|
||||
clean_model = str(model).strip().upper()
|
||||
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
|
||||
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
|
||||
try:
|
||||
resp = await client.get(cls.API_URL, params=params, timeout=15)
|
||||
if resp.status_code == 200 and resp.json():
|
||||
return resp.json()[0]
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def run(cls):
|
||||
logger.info("🚀 Master Enricher INDUL (Atomi mentés üzemmód)...")
|
||||
|
||||
# 1. Csak az ID-kat kérjük le, hogy ne tartsuk nyitva a tranzakciót feleslegesen
|
||||
async with SessionLocal() as main_db:
|
||||
stmt = select(VehicleModelDefinition.id).where(
|
||||
VehicleModelDefinition.status == "unverified"
|
||||
).limit(50)
|
||||
res = await main_db.execute(stmt)
|
||||
ids = res.scalars().all()
|
||||
|
||||
if not ids:
|
||||
logger.info("😴 Nincs dúsítandó adat.")
|
||||
return
|
||||
|
||||
# 2. Egyesével dolgozzuk fel a rekordokat saját session-ben
|
||||
for m_id in ids:
|
||||
async with SessionLocal() as db:
|
||||
try:
|
||||
master = await db.get(VehicleModelDefinition, m_id)
|
||||
if not master:
|
||||
continue
|
||||
|
||||
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name} (ID: {m_id})")
|
||||
data_found = False
|
||||
|
||||
# A: RDW fázis
|
||||
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
|
||||
if rdw_data:
|
||||
master.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or None
|
||||
master.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or None
|
||||
data_found = True
|
||||
|
||||
# B: AI fázis (ha hiányzik adat vagy pontosítani kell)
|
||||
if not data_found or master.engine_capacity is None:
|
||||
ai_data = await AIService.get_clean_vehicle_data(
|
||||
master.make, master.marketing_name, master.vehicle_type
|
||||
)
|
||||
if ai_data:
|
||||
master.marketing_name = ai_data.get("marketing_name", master.marketing_name)
|
||||
master.technical_code = ai_data.get("technical_code") or master.technical_code or "N/A"
|
||||
master.engine_capacity = ai_data.get("ccm") or master.engine_capacity
|
||||
master.power_kw = ai_data.get("kw") or master.power_kw
|
||||
master.specifications = ai_data.get("maintenance", {})
|
||||
data_found = True
|
||||
|
||||
# C: Mentés és véglegesítés
|
||||
if data_found:
|
||||
master.status = "ai_enriched"
|
||||
master.updated_at = datetime.datetime.now()
|
||||
await db.commit() # AZONNALI COMMIT A LEMEZRE
|
||||
logger.info(f"✅ Sikeresen mentve: {master.marketing_name} (CCM: {master.engine_capacity})")
|
||||
else:
|
||||
logger.warning(f"⚠️ Nem találtam adatot az ID {m_id} esetében.")
|
||||
|
||||
except IntegrityError:
|
||||
await db.rollback()
|
||||
logger.warning(f"🚫 Duplikáció vagy Constraint hiba (ID: {m_id}). Kihagyva.")
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Váratlan hiba az ID {m_id} esetében: {e}")
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
logger.info("🏁 50-es batch feldolgozva.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(TechEnricher.run())
|
||||
310
backend/migrations/versions/c64b951dbb86_add_mdm_merge_fields.py
Normal file
310
backend/migrations/versions/c64b951dbb86_add_mdm_merge_fields.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""add_mdm_merge_fields
|
||||
|
||||
Revision ID: c64b951dbb86
|
||||
Revises: f30c0005c446
|
||||
Create Date: 2026-02-17 21:33:35.453033
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c64b951dbb86'
|
||||
down_revision: Union[str, Sequence[str], None] = 'f30c0005c446'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_assignments', 'branches', ['branch_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
|
||||
op.drop_constraint(op.f('assets_current_organization_id_fkey'), 'assets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'assets', 'organizations', ['current_organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('branches_address_id_fkey'), 'branches', type_='foreignkey')
|
||||
op.drop_constraint(op.f('branches_organization_id_fkey'), 'branches', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'branches', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'branches', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('feature_definitions_vehicle_type_id_fkey'), 'feature_definitions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'feature_definitions', 'vehicle_types', ['vehicle_type_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('financial_ledger_user_id_fkey'), 'financial_ledger', type_='foreignkey')
|
||||
op.drop_constraint(op.f('financial_ledger_person_id_fkey'), 'financial_ledger', type_='foreignkey')
|
||||
op.drop_constraint(op.f('financial_ledger_related_agent_id_fkey'), 'financial_ledger', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'financial_ledger', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'financial_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'financial_ledger', 'users', ['related_agent_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('model_feature_maps_model_id_fkey'), 'model_feature_maps', type_='foreignkey')
|
||||
op.drop_constraint(op.f('model_feature_maps_feature_id_fkey'), 'model_feature_maps', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'model_feature_maps', 'vehicle_model_definitions', ['model_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'model_feature_maps', 'feature_definitions', ['feature_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('operational_logs_user_id_fkey'), 'operational_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'operational_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='SET NULL')
|
||||
op.drop_constraint(op.f('org_sales_assignments_organization_id_fkey'), 'org_sales_assignments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('org_sales_assignments_agent_user_id_fkey'), 'org_sales_assignments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'org_sales_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'org_sales_assignments', 'users', ['agent_user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('organization_financials_organization_id_fkey'), 'organization_financials', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'organization_financials', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('organization_members', 'role',
|
||||
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
|
||||
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(op.f('organization_members_person_id_fkey'), 'organization_members', type_='foreignkey')
|
||||
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
|
||||
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'organization_members', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('organizations', 'org_type',
|
||||
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
|
||||
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
|
||||
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('pending_actions_requester_id_fkey'), 'pending_actions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('pending_actions_approver_id_fkey'), 'pending_actions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'pending_actions', 'users', ['approver_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'pending_actions', 'users', ['requester_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('security_audit_logs_actor_id_fkey'), 'security_audit_logs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('security_audit_logs_confirmed_by_id_fkey'), 'security_audit_logs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('security_audit_logs_target_id_fkey'), 'security_audit_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'security_audit_logs', 'users', ['confirmed_by_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'security_audit_logs', 'users', ['actor_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'security_audit_logs', 'users', ['target_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('service_expertises_service_id_fkey'), 'service_expertises', type_='foreignkey')
|
||||
op.drop_constraint(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'service_expertises', 'service_profiles', ['service_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('service_profiles_organization_id_fkey'), 'service_profiles', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'service_profiles', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('social_accounts_user_id_fkey'), 'social_accounts', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'social_accounts', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
|
||||
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
|
||||
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
|
||||
op.drop_constraint(op.f('users_referred_by_id_fkey'), 'users', type_='foreignkey')
|
||||
op.drop_constraint(op.f('users_current_sales_agent_id_fkey'), 'users', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'users', 'users', ['current_sales_agent_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'users', 'users', ['referred_by_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('vehicle_catalog_master_definition_id_fkey'), 'vehicle_catalog', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'vehicle_catalog', 'vehicle_model_definitions', ['master_definition_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.add_column('vehicle_model_definitions', sa.Column('parent_id', sa.Integer(), nullable=True))
|
||||
op.add_column('vehicle_model_definitions', sa.Column('year_from', sa.Integer(), nullable=True))
|
||||
op.add_column('vehicle_model_definitions', sa.Column('year_to', sa.Integer(), nullable=True))
|
||||
op.add_column('vehicle_model_definitions', sa.Column('synonyms', sa.JSON(), server_default=sa.text("'[]'::jsonb"), nullable=True))
|
||||
op.create_index('idx_vmd_lookup', 'vehicle_model_definitions', ['make', 'technical_code'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_vehicle_model_definitions_year_from'), 'vehicle_model_definitions', ['year_from'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_vehicle_model_definitions_year_to'), 'vehicle_model_definitions', ['year_to'], unique=False, schema='data')
|
||||
op.drop_constraint(op.f('vehicle_model_definitions_vehicle_type_id_fkey'), 'vehicle_model_definitions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'vehicle_model_definitions', 'vehicle_model_definitions', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'vehicle_model_definitions', 'vehicle_types', ['vehicle_type_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
|
||||
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
|
||||
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
||||
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
|
||||
op.drop_constraint(None, 'vehicle_model_definitions', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'vehicle_model_definitions', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('vehicle_model_definitions_vehicle_type_id_fkey'), 'vehicle_model_definitions', 'vehicle_types', ['vehicle_type_id'], ['id'])
|
||||
op.drop_index(op.f('ix_data_vehicle_model_definitions_year_to'), table_name='vehicle_model_definitions', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_model_definitions_year_from'), table_name='vehicle_model_definitions', schema='data')
|
||||
op.drop_index('idx_vmd_lookup', table_name='vehicle_model_definitions', schema='data')
|
||||
op.drop_column('vehicle_model_definitions', 'synonyms')
|
||||
op.drop_column('vehicle_model_definitions', 'year_to')
|
||||
op.drop_column('vehicle_model_definitions', 'year_from')
|
||||
op.drop_column('vehicle_model_definitions', 'parent_id')
|
||||
op.drop_constraint(None, 'vehicle_catalog', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('vehicle_catalog_master_definition_id_fkey'), 'vehicle_catalog', 'vehicle_model_definitions', ['master_definition_id'], ['id'])
|
||||
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('users_current_sales_agent_id_fkey'), 'users', 'users', ['current_sales_agent_id'], ['id'])
|
||||
op.create_foreign_key(op.f('users_referred_by_id_fkey'), 'users', 'users', ['referred_by_id'], ['id'])
|
||||
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
|
||||
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
|
||||
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'social_accounts', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('social_accounts_user_id_fkey'), 'social_accounts', 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
||||
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
|
||||
op.drop_constraint(None, 'service_profiles', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('service_profiles_organization_id_fkey'), 'service_profiles', 'organizations', ['organization_id'], ['id'])
|
||||
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'service_expertises', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('service_expertises_expertise_id_fkey'), 'service_expertises', 'expertise_tags', ['expertise_id'], ['id'])
|
||||
op.create_foreign_key(op.f('service_expertises_service_id_fkey'), 'service_expertises', 'service_profiles', ['service_id'], ['id'])
|
||||
op.drop_constraint(None, 'security_audit_logs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'security_audit_logs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'security_audit_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('security_audit_logs_target_id_fkey'), 'security_audit_logs', 'users', ['target_id'], ['id'])
|
||||
op.create_foreign_key(op.f('security_audit_logs_confirmed_by_id_fkey'), 'security_audit_logs', 'users', ['confirmed_by_id'], ['id'])
|
||||
op.create_foreign_key(op.f('security_audit_logs_actor_id_fkey'), 'security_audit_logs', 'users', ['actor_id'], ['id'])
|
||||
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
|
||||
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
|
||||
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'pending_actions', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('pending_actions_approver_id_fkey'), 'pending_actions', 'users', ['approver_id'], ['id'])
|
||||
op.create_foreign_key(op.f('pending_actions_requester_id_fkey'), 'pending_actions', 'users', ['requester_id'], ['id'])
|
||||
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
|
||||
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
|
||||
op.alter_column('organizations', 'org_type',
|
||||
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
|
||||
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('organization_members_person_id_fkey'), 'organization_members', 'persons', ['person_id'], ['id'])
|
||||
op.alter_column('organization_members', 'role',
|
||||
existing_type=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole', schema='data', inherit_schema=True),
|
||||
type_=postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'MECHANIC', 'RECEPTIONIST', name='orguserrole'),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(None, 'organization_financials', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('organization_financials_organization_id_fkey'), 'organization_financials', 'organizations', ['organization_id'], ['id'])
|
||||
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
|
||||
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
|
||||
op.drop_constraint(None, 'org_sales_assignments', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'org_sales_assignments', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('org_sales_assignments_agent_user_id_fkey'), 'org_sales_assignments', 'users', ['agent_user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('org_sales_assignments_organization_id_fkey'), 'org_sales_assignments', 'organizations', ['organization_id'], ['id'])
|
||||
op.drop_constraint(None, 'operational_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('operational_logs_user_id_fkey'), 'operational_logs', 'users', ['user_id'], ['id'], ondelete='SET NULL')
|
||||
op.drop_constraint(None, 'model_feature_maps', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'model_feature_maps', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('model_feature_maps_feature_id_fkey'), 'model_feature_maps', 'feature_definitions', ['feature_id'], ['id'])
|
||||
op.create_foreign_key(op.f('model_feature_maps_model_id_fkey'), 'model_feature_maps', 'vehicle_model_definitions', ['model_id'], ['id'])
|
||||
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
|
||||
op.drop_constraint(None, 'financial_ledger', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'financial_ledger', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'financial_ledger', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('financial_ledger_related_agent_id_fkey'), 'financial_ledger', 'users', ['related_agent_id'], ['id'])
|
||||
op.create_foreign_key(op.f('financial_ledger_person_id_fkey'), 'financial_ledger', 'persons', ['person_id'], ['id'])
|
||||
op.create_foreign_key(op.f('financial_ledger_user_id_fkey'), 'financial_ledger', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'feature_definitions', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('feature_definitions_vehicle_type_id_fkey'), 'feature_definitions', 'vehicle_types', ['vehicle_type_id'], ['id'])
|
||||
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
|
||||
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
|
||||
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'branches', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('branches_organization_id_fkey'), 'branches', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('branches_address_id_fkey'), 'branches', 'addresses', ['address_id'], ['id'])
|
||||
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('assets_current_organization_id_fkey'), 'assets', 'organizations', ['current_organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_assignments_branch_id_fkey'), 'asset_assignments', 'branches', ['branch_id'], ['id'])
|
||||
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
@@ -24,4 +24,5 @@ fastapi-limiter==0.1.5
|
||||
pyotp
|
||||
cryptography
|
||||
GeoAlchemy2>=0.14.0
|
||||
google-generativeai
|
||||
google-generativeai
|
||||
google-genai
|
||||
Reference in New Issue
Block a user